diff --git a/.github/actions/await-http-resource/action.yml b/.github/actions/await-http-resource/action.yml new file mode 100644 index 000000000000..7d2b3462b537 --- /dev/null +++ b/.github/actions/await-http-resource/action.yml @@ -0,0 +1,20 @@ +name: Await HTTP Resource +description: Waits for an HTTP resource to be available (a HEAD request succeeds) +inputs: + url: + description: 'The URL of the resource to await' + required: true +runs: + using: composite + steps: + - name: Await HTTP resource + shell: bash + run: | + url=${{ inputs.url }} + echo "Waiting for $url" + until curl --fail --head --silent ${{ inputs.url }} > /dev/null + do + echo "." + sleep 60 + done + echo "$url is available" diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 000000000000..78188ae631b9 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,56 @@ +name: 'Build' +description: 'Builds the project, optionally publishing it to a local deployment repository' +inputs: + java-version: + required: false + default: '17' + description: 'The Java version to compile and test with' + java-early-access: + required: false + default: 'false' + description: 'Whether the Java version is in early access' + java-toolchain: + required: false + default: 'false' + description: 'Whether a Java toolchain should be used' + publish: + required: false + default: 'false' + description: 'Whether to publish artifacts ready for deployment to Artifactory' + develocity-access-key: + required: false + description: 'The access key for authentication with ge.spring.io' +outputs: + build-scan-url: + description: 'The 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: 'The 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-version: ${{ inputs.java-version }} + java-early-access: ${{ inputs.java-early-access }} + java-toolchain: ${{ inputs.java-toolchain }} + - name: Build + id: build + if: ${{ inputs.publish == 'false' }} + shell: bash + run: ./gradlew check antora + - name: Publish + id: publish + if: ${{ inputs.publish == 'true' }} + shell: bash + run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository + - name: Read Version From gradle.properties + id: read-version + shell: bash + run: | + version=$(sed -n 's/version=\(.*\)/\1/p' gradle.properties) + echo "Version is $version" + echo "version=$version" >> $GITHUB_OUTPUT diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml new file mode 100644 index 000000000000..e0120764f1e4 --- /dev/null +++ b/.github/actions/create-github-release/action.yml @@ -0,0 +1,23 @@ +name: Create GitHub Release +description: Create the release on GitHub with a changelog +inputs: + milestone: + description: Name of the GitHub milestone for which a release will be created + required: true + token: + description: Token to use for authentication with GitHub + required: true +runs: + using: composite + steps: + - name: Generate Changelog + uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 + with: + milestone: ${{ inputs.milestone }} + token: ${{ inputs.token }} + config-file: .github/actions/create-github-release/changelog-generator.yml + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ inputs.token }} + shell: bash + run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md diff --git a/ci/config/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml similarity index 73% rename from ci/config/changelog-generator.yml rename to .github/actions/create-github-release/changelog-generator.yml index 2252d20802e4..725c40966679 100644 --- a/ci/config/changelog-generator.yml +++ b/.github/actions/create-github-release/changelog-generator.yml @@ -17,4 +17,12 @@ changelog: - "type: dependency-upgrade" contributors: exclude: - names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"] + names: + - "bclozel" + - "jhoeller" + - "poutsma" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "simonbasle" + - "snicoll" diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml new file mode 100644 index 000000000000..e4dec8c7f0dd --- /dev/null +++ b/.github/actions/prepare-gradle-build/action.yml @@ -0,0 +1,49 @@ +name: 'Prepare Gradle Build' +description: 'Prepares a Gradle build. Sets up Java and Gradle and configures Gradle properties' +inputs: + java-version: + required: false + default: '17' + description: 'The Java version to use for the build' + java-early-access: + required: false + default: 'false' + description: 'Whether the Java version is in early access' + java-toolchain: + required: false + default: 'false' + description: 'Whether a Java toolchain should be used' + develocity-access-key: + required: false + description: 'The access key for authentication with ge.spring.io' +runs: + using: composite + steps: + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || 'liberica' }} + java-version: | + ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} + ${{ inputs.java-toolchain == 'true' && '17' || '' }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + with: + cache-read-only: false + develocity-access-key: ${{ inputs.develocity-access-key }} + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties + - name: Configure Toolchain Properties + if: ${{ inputs.java-toolchain == 'true' }} + shell: bash + run: | + echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml new file mode 100644 index 000000000000..d1389776397a --- /dev/null +++ b/.github/actions/send-notification/action.yml @@ -0,0 +1,33 @@ +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' + run-name: + description: 'Name of the run to include in the notification' + default: ${{ format('{0} {1}', github.ref_name, github.job) }} +runs: + using: composite + steps: + - 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 + if: ${{ inputs.status == 'success' }} + 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 + if: ${{ inputs.status == 'failure' }} + 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 + if: ${{ inputs.status == 'cancelled' }} + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was cancelled"}' || true diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml new file mode 100644 index 000000000000..d4e86caf1196 --- /dev/null +++ b/.github/actions/sync-to-maven-central/action.yml @@ -0,0 +1,43 @@ +name: Sync to Maven Central +description: Syncs a release to Maven Central and waits for it to be available for use +inputs: + jfrog-cli-config-token: + description: 'Config token for the JFrog CLI' + required: true + spring-framework-version: + description: 'The version of Spring Framework that is being synced to Central' + required: true + ossrh-s01-token-username: + description: 'Username for authentication with s01.oss.sonatype.org' + required: true + ossrh-s01-token-password: + description: 'Password for authentication with s01.oss.sonatype.org' + required: true + ossrh-s01-staging-profile: + description: 'Staging profile to use when syncing to Central' + required: true +runs: + using: composite + steps: + - name: Set Up JFrog CLI + uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1 + env: + JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }} + - name: Download Release Artifacts + shell: bash + run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}' + - name: Sync + uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1 + with: + username: ${{ inputs.ossrh-s01-token-username }} + password: ${{ inputs.ossrh-s01-token-password }} + staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }} + create: true + upload: true + close: true + release: true + generate-checksums: true + - name: Await + uses: ./.github/actions/await-http-resource + with: + url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }} diff --git a/.github/actions/sync-to-maven-central/artifacts.spec b/.github/actions/sync-to-maven-central/artifacts.spec new file mode 100644 index 000000000000..f5c16ed5137a --- /dev/null +++ b/.github/actions/sync-to-maven-central/artifacts.spec @@ -0,0 +1,20 @@ +{ + "files": [ + { + "aql": { + "items.find": { + "$and": [ + { + "@build.name": "${buildName}", + "@build.number": "${buildNumber}", + "path": { + "$nmatch": "org/springframework/framework-api/*" + } + } + ] + } + }, + "target": "nexus/" + } + ] +} diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml index 153c63247652..4d025ece2ceb 100644 --- a/.github/workflows/backport-bot.yml +++ b/.github/workflows/backport-bot.yml @@ -18,11 +18,13 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '17' + distribution: 'liberica' + java-version: 17 - name: Download BackportBot run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar - name: Backport diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml new file mode 100644 index 000000000000..721b759646e3 --- /dev/null +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -0,0 +1,58 @@ +name: Build and Deploy Snapshot +on: + push: + branches: + - 6.1.x +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-deploy-snapshot: + name: Build and Deploy Snapshot + runs-on: ubuntu-latest + timeout-minutes: 60 + if: ${{ github.repository == 'spring-projects/spring-framework' }} + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + publish: true + - name: Deploy + uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 + with: + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + build-name: 'spring-framework-6.1.x' + 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 + if: always() + with: + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + status: ${{ job.status }} + build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }} + run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + verify: + name: Verify + needs: build-and-deploy-snapshot + uses: ./.github/workflows/verify.yml + secrets: + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + with: + version: ${{ needs.build-and-deploy-snapshot.outputs.version }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..23f00c9b285b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI +on: + push: + branches: + - 6.1.x +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + ci: + name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + runs-on: ${{ matrix.os.id }} + timeout-minutes: 60 + if: ${{ github.repository == 'spring-projects/spring-framework' }} + strategy: + matrix: + os: + - id: ubuntu-latest + name: Linux + java: + - version: 17 + toolchain: false + - version: 21 + toolchain: true + - version: 22 + toolchain: true + - version: 23 + early-access: true + toolchain: true + exclude: + - os: + name: Linux + java: + version: 17 + steps: + - name: Prepare Windows runner + if: ${{ runner.os == 'Windows' }} + run: | + git config --global core.autocrlf true + git config --global core.longPaths true + Stop-Service -name Docker + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build + id: build + uses: ./.github/actions/build + with: + java-version: ${{ matrix.java.version }} + java-early-access: ${{ matrix.java.early-access || 'false' }} + java-toolchain: ${{ matrix.java.toolchain }} + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + - name: Send Notification + uses: ./.github/actions/send-notification + if: always() + 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) }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f3e899c4fe0e..1d66f04806b0 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,12 +1,14 @@ name: Deploy Docs on: push: - branches-ignore: [ gh-pages ] - tags: '**' + branches: + - 'main' + - '*.x' + - '!gh-pages' + tags: + - 'v*' repository_dispatch: types: request-build-reference # legacy - schedule: - - cron: '0 10 * * *' # Once per day at 10am UTC workflow_dispatch: permissions: actions: write @@ -15,8 +17,8 @@ jobs: runs-on: ubuntu-latest if: github.repository_owner == 'spring-projects' steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Check out code + uses: actions/checkout@v4 with: ref: docs-build fetch-depth: 1 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index c80a7e5278d0..000000000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "Validate Gradle Wrapper" -on: [push, pull_request] - -permissions: - contents: read - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..81b17ee1dc95 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release +on: + push: + tags: + - v6.1.[0-9]+ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-stage-release: + if: ${{ github.repository == 'spring-projects/spring-framework' }} + name: Build and Stage Release + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + publish: true + - name: Stage Release + uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 + with: + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}} + repository: 'libs-staging-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 + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + verify: + name: Verify + needs: build-and-stage-release + uses: ./.github/workflows/verify.yml + with: + staging: true + version: ${{ needs.build-and-stage-release.outputs.version }} + secrets: + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + sync-to-maven-central: + name: Sync to Maven Central + needs: + - build-and-stage-release + - verify + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Sync to Maven Central + uses: ./.github/actions/sync-to-maven-central + with: + jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }} + ossrh-s01-staging-profile: ${{ secrets.OSSRH_S01_STAGING_PROFILE }} + ossrh-s01-token-password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + ossrh-s01-token-username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }} + promote-release: + name: Promote Release + needs: + - build-and-stage-release + - sync-to-maven-central + runs-on: ubuntu-latest + steps: + - name: Set up JFrog CLI + uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Promote build + run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local + create-github-release: + name: Create GitHub Release + needs: + - build-and-stage-release + - promote-release + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Create GitHub Release + uses: ./.github/actions/create-github-release + with: + milestone: ${{ needs.build-and-stage-release.outputs.version }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/validate-gradle-wrapper.yml b/.github/workflows/validate-gradle-wrapper.yml new file mode 100644 index 000000000000..7a473b3afe72 --- /dev/null +++ b/.github/workflows/validate-gradle-wrapper.yml @@ -0,0 +1,11 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] +permissions: + contents: read +jobs: + validation: + name: "Validate Gradle Wrapper" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 000000000000..b9b1e17de783 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,71 @@ +name: Verify +on: + workflow_call: + inputs: + version: + required: true + type: string + staging: + required: false + default: false + type: boolean + secrets: + repository-username: + required: false + repository-password: + required: false + google-chat-webhook-url: + required: true + token: + required: true +jobs: + verify: + name: Verify + runs-on: ubuntu-latest + steps: + - name: Check Out Release Verification Tests + uses: actions/checkout@v4 + with: + repository: spring-projects/spring-framework-release-verification + ref: 'v0.0.2' + token: ${{ secrets.token }} + - name: Check Out Send Notification Action + uses: actions/checkout@v4 + with: + path: spring-framework + sparse-checkout: .github/actions/send-notification + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: 17 + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + with: + cache-read-only: false + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + - name: Run Release Verification Tests + env: + RVT_VERSION: ${{ inputs.version }} + RVT_RELEASE_TYPE: oss + RVT_STAGING: ${{ inputs.staging }} + RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }} + RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }} + run: ./gradlew spring-framework-release-verification-tests:test + - name: Upload Build Reports on Failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: build-reports + path: '**/build/reports/' + - name: Send Notification + uses: ./spring-framework/.github/actions/send-notification + if: failure() + with: + webhook-url: ${{ secrets.google-chat-webhook-url }} + status: ${{ job.status }} + run-name: ${{ format('{0} | Verification | {1}', github.ref_name, inputs.version) }} diff --git a/.gitignore b/.gitignore index 14252754d456..549d5756e164 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ atlassian-ide-plugin.xml .vscode/ cached-antora-playbook.yml + +node_modules diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 0d65c926d2d5..000000000000 --- a/.mailmap +++ /dev/null @@ -1,36 +0,0 @@ -Juergen Hoeller -Juergen Hoeller -Juergen Hoeller -Rossen Stoyanchev -Rossen Stoyanchev -Rossen Stoyanchev -Phillip Webb -Phillip Webb -Phillip Webb -Chris Beams -Chris Beams -Chris Beams -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Oliver Drotbohm -Oliver Drotbohm -Oliver Drotbohm -Oliver Drotbohm -Dave Syer -Dave Syer -Dave Syer -Dave Syer -Andy Clement -Andy Clement -Andy Clement -Andy Clement -Sam Brannen -Sam Brannen -Sam Brannen -Simon Basle -Simon Baslé - -Nick Williams diff --git a/.sdkmanrc b/.sdkmanrc index 3c4d046f46e3..828308d277ec 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=17.0.8.1-librca +java=17.0.12-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 6edef2962b98..93e430897e83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ First off, thank you for taking the time to contribute! :+1: :tada: This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc). 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 @@ -123,13 +123,13 @@ define the source file coding standards we use along with some IDEA editor setti ### Reference Docs -The reference documentation is in the [framework-docs/src/docs/asciidoc](framework-docs/src/docs/asciidoc) directory, in -[Asciidoctor](https://asciidoctor.org/) format. For trivial changes, you may be able to browse, -edit source files, and submit directly from GitHub. +The reference documentation is authored in [Asciidoctor](https://asciidoctor.org/) format +using [Antora](https://docs.antora.org/antora/latest/). The source files for the documentation +reside in the [framework-docs/modules/ROOT](framework-docs/modules/ROOT) directory. For +trivial changes, you may be able to browse, edit source files, and submit directly from GitHub. -When making changes locally, execute `./gradlew :framework-docs:asciidoctor` and then browse the result under -`framework-docs/build/docs/ref-docs/html5/index.html`. +When making changes locally, execute `./gradlew antora` and then browse the results under +`framework-docs/build/site/index.html`. Asciidoctor also supports live editing. For more details see [AsciiDoc Tooling](https://docs.asciidoctor.org/asciidoctor/latest/tooling/). - diff --git a/README.md b/README.md index 4fd549f124a3..672721533336 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.0.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) +# Spring Framework [![Build Status](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=6.1.x)](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3A6.1.x) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring". -Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. Please read the [Overview](https://docs.spring.io/spring/docs/current/spring-framework-reference/overview.html#spring-introduction) section as reference for a more complete introduction. +Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. Please read the [Overview](https://docs.spring.io/spring-framework/reference/overview.html) section of the reference documentation for a more complete introduction. ## 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](CODE_OF_CONDUCT.adoc). 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 @@ -14,7 +14,7 @@ For access to artifacts or a distribution zip, see the [Spring Framework Artifac ## Documentation -The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](framework-docs/src/docs/asciidoc)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an +The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/reference/) and [source](framework-docs/modules/ROOT)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an [API reference](https://docs.spring.io/spring-framework/docs/current/javadoc-api/). There are also [guides and tutorials](https://spring.io/guides) across Spring projects. ## Micro-Benchmarks @@ -31,7 +31,7 @@ Information regarding CI builds can be found in the [Spring Framework Concourse ## Stay in Touch -Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on Twitter. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [news feed](https://spring.io/blog/category/news). +Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on 𝕏. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [releases feed](https://spring.io/blog/category/releases). ## License diff --git a/build.gradle b/build.gradle index 6ffc617a46a3..2891f6ac7779 100644 --- a/build.gradle +++ b/build.gradle @@ -1,22 +1,26 @@ plugins { - id 'io.spring.nohttp' version '0.0.11' - id 'io.freefair.aspectj' version '8.0.1' apply false + id 'io.freefair.aspectj' version '8.4' apply false // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false - id 'org.jetbrains.dokka' version '1.8.10' - id 'org.unbroken-dome.xjc' version '2.0.0' apply false - id 'com.github.ben-manes.versions' version '0.49.0' - id 'com.github.johnrengelman.shadow' version '8.1.1' apply false + id 'org.jetbrains.dokka' version '1.8.20' + id 'com.github.ben-manes.versions' version '0.51.0' + id 'com.github.bjornvester.xjc' version '1.8.2' apply false id 'de.undercouch.download' version '5.4.0' - id 'me.champeau.jmh' version '0.7.1' apply false + id 'io.github.goooler.shadow' version '8.1.8' apply false + id 'me.champeau.jmh' version '0.7.2' apply false + id 'me.champeau.mrjar' version '0.1.1' } ext { moduleProjects = subprojects.findAll { it.name.startsWith("spring-") } - javaProjects = subprojects - project(":framework-bom") - project(":framework-platform") + javaProjects = subprojects.findAll { !it.name.startsWith("framework-") } } +description = "Spring Framework" + configure(allprojects) { project -> + apply plugin: "org.springframework.build.localdev" + group = "org.springframework" repositories { mavenCentral() maven { @@ -41,16 +45,7 @@ configure(allprojects) { project -> } } -configure([rootProject] + javaProjects) { project -> - group = "org.springframework" - - apply plugin: "java" - apply plugin: "java-test-fixtures" - apply plugin: "checkstyle" - apply plugin: 'org.springframework.build.conventions' - apply from: "${rootDir}/gradle/toolchains.gradle" - apply from: "${rootDir}/gradle/ide.gradle" - +configure(allprojects - project(":framework-platform")) { configurations { dependencyManagement { canBeConsumed = false @@ -59,36 +54,19 @@ configure([rootProject] + javaProjects) { project -> } matching { it.name.endsWith("Classpath") }.all { it.extendsFrom(dependencyManagement) } } - - test { - useJUnitPlatform() - include(["**/*Tests.class", "**/*Test.class"]) - systemProperty("java.awt.headless", "true") - systemProperty("testGroups", project.properties.get("testGroups")) - systemProperty("io.netty.leakDetection.level", "paranoid") - systemProperty("io.netty5.leakDetectionLevel", "paranoid") - systemProperty("io.netty5.leakDetection.targetRecords", "32") - systemProperty("io.netty5.buffer.lifecycleTracingEnabled", "true") - systemProperty("io.netty5.buffer.leakDetectionEnabled", "true") - jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED", - "--add-opens=java.base/java.util=ALL-UNNAMED"]) - } - - checkstyle { - toolVersion = "10.12.5" - configDirectory.set(rootProject.file("src/checkstyle")) - } - - tasks.named("checkstyleMain").configure { - maxHeapSize = "1g" + dependencies { + dependencyManagement(enforcedPlatform(dependencies.project(path: ":framework-platform"))) } +} - tasks.named("checkstyleTest").configure { - maxHeapSize = "1g" - } +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 { - dependencyManagement(enforcedPlatform(dependencies.project(path: ":framework-platform"))) testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.junit.jupiter:junit-jupiter-params") testImplementation("org.junit.platform:junit-platform-suite-api") @@ -106,66 +84,34 @@ configure([rootProject] + javaProjects) { project -> // JSR-305 only used for non-required meta-annotations compileOnly("com.google.code.findbugs:jsr305") testCompileOnly("com.google.code.findbugs:jsr305") - checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.39") } ext.javadocLinks = [ "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://jakarta.ee/specifications/platform/9/apidocs/", - "https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ and weblogic.* packages - "https://www.ibm.com/docs/api/v1/content/SSEQTP_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/", // com.ibm.* - "https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", // org.jboss.resource.* "https://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://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.14.1/", - "https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.14.1/", - "https://www.javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.14.1/", + "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.9.3/api/", + // "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://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", "https://r2dbc.io/spec/1.0.0.RELEASE/api/", // Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages, // but since 6.0 JSR 250 annotations such as @Resource and @PostConstruct have been replaced by their // JakartaEE equivalents in the jakarta.annotation package. - "https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/" + //"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/" ] as String[] } configure(moduleProjects) { project -> apply from: "${rootDir}/gradle/spring-module.gradle" } - -configure(rootProject) { - description = "Spring Framework" - - apply plugin: "io.spring.nohttp" - apply plugin: 'org.springframework.build.api-diff' - - nohttp { - source.exclude "**/test-output/**" - source.exclude "**/.gradle/**" - allowlistFile = project.file("src/nohttp/allowlist.lines") - def rootPath = file(rootDir).toPath() - def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/buildSrc" - projectDirs.forEach { dir -> - [ 'bin', 'build', 'out', '.settings' ] - .collect { rootPath.relativize(new File(dir, it).toPath()) } - .forEach { source.exclude "$it/**" } - [ '.classpath', '.project' ] - .collect { rootPath.relativize(new File(dir, it).toPath()) } - .forEach { source.exclude "$it" } - } - } - - tasks.named("checkstyleNohttp").configure { - maxHeapSize = "1g" - } - -} diff --git a/buildSrc/README.md b/buildSrc/README.md index 90dfdd23db84..9e35b5b766cf 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -22,21 +22,6 @@ but doesn't affect the classpath of dependent projects. This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly` configurations are preferred. -### API Diff - -This plugin uses the [Gradle JApiCmp](https://github.com/melix/japicmp-gradle-plugin) plugin -to generate API Diff reports for each Spring Framework module. This plugin is applied once on the root -project and creates tasks in each framework module. Unlike previous versions of this part of the build, -there is no need for checking out a specific tag. The plugin will fetch the JARs we want to compare the -current working version with. You can generate the reports for all modules or a single module: - -``` -./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE -./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE -``` - -The reports are located under `build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/`. - ### RuntimeHints Java Agent diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index e21f9231a98a..19d41d438fe4 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-gradle-plugin' + id 'checkstyle' } repositories { @@ -17,22 +18,24 @@ ext { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") - implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}") - implementation "me.champeau.gradle:japicmp-gradle-plugin:0.3.0" - implementation "org.gradle:test-retry-gradle-plugin:1.4.1" + 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 "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}" + implementation "io.spring.nohttp:nohttp-gradle:0.0.11" } gradlePlugin { plugins { - apiDiffPlugin { - id = "org.springframework.build.api-diff" - implementationClass = "org.springframework.build.api.ApiDiffPlugin" - } conventionsPlugin { id = "org.springframework.build.conventions" implementationClass = "org.springframework.build.ConventionsPlugin" } + localDevPlugin { + id = "org.springframework.build.localdev" + implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin" + } optionalDependenciesPlugin { id = "org.springframework.build.optional-dependencies" implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin" diff --git a/buildSrc/config/checkstyle/checkstyle.xml b/buildSrc/config/checkstyle/checkstyle.xml new file mode 100644 index 000000000000..c63f232e1e70 --- /dev/null +++ b/buildSrc/config/checkstyle/checkstyle.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties index 160890028a4d..eacaf4f5b871 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -1 +1,2 @@ org.gradle.caching=true +javaFormatVersion=0.0.42 diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java new file mode 100644 index 000000000000..db28118a74be --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.File; +import java.nio.file.Path; +import java.util.List; + +import io.spring.javaformat.gradle.SpringJavaFormatPlugin; +import io.spring.nohttp.gradle.NoHttpExtension; +import io.spring.nohttp.gradle.NoHttpPlugin; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.plugins.quality.Checkstyle; +import org.gradle.api.plugins.quality.CheckstyleExtension; +import org.gradle.api.plugins.quality.CheckstylePlugin; + +/** + * {@link Plugin} that applies conventions for checkstyle. + * + * @author Brian Clozel + */ +public class CheckstyleConventions { + + /** + * Applies the Spring Java Format and Checkstyle plugins with the project conventions. + * @param project the current project + */ + public void apply(Project project) { + project.getPlugins().withType(JavaBasePlugin.class, (java) -> { + if (project.getRootProject() == project) { + configureNoHttpPlugin(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.18.1"); + checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); + String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); + DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); + checkstyleDependencies.add( + project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version)); + }); + } + + private static void configureNoHttpPlugin(Project project) { + project.getPlugins().apply(NoHttpPlugin.class); + NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class); + noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines")); + noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", + "**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**"); + List buildFolders = List.of("bin", "build", "out"); + project.allprojects(subproject -> { + Path rootPath = project.getRootDir().toPath(); + Path projectPath = rootPath.relativize(subproject.getProjectDir().toPath()); + for (String buildFolder : buildFolders) { + Path innerBuildDir = projectPath.resolve(buildFolder); + noHttp.getSource().exclude(innerBuildDir + File.separator + "**"); + } + }); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java index e54ddce55974..34978ba377d2 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,8 @@ * 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 TestConventions} - * are applied. - * When the {@link JavaBasePlugin} is applied, the conventions in {@link JavaConventions} - * are applied. + *

When the {@link JavaBasePlugin} is applied, the conventions in {@link CheckstyleConventions}, + * {@link TestConventions} and {@link JavaConventions} are applied. * When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions} * are applied. * @@ -38,8 +36,10 @@ public class ConventionsPlugin implements Plugin { @Override public void apply(Project project) { + new CheckstyleConventions().apply(project); new JavaConventions().apply(project); new KotlinConventions().apply(project); new TestConventions().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 134f99fe9fac..60b791799f52 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,10 @@ import org.gradle.api.Project; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JvmVendorSpec; /** * {@link Plugin} that applies conventions for compiling Java sources in Spring Framework. @@ -68,6 +71,10 @@ public void apply(Project project) { * @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.getTasks().withType(JavaCompile.class) .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME)) .forEach(compileTask -> { diff --git a/buildSrc/src/main/java/org/springframework/build/TestConventions.java b/buildSrc/src/main/java/org/springframework/build/TestConventions.java index 6f949bf43dcf..1283d233765d 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-2024 the original author 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.build; +import java.util.Map; + import org.gradle.api.Project; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.tasks.testing.Test; @@ -41,11 +43,38 @@ void apply(Project project) { private void configureTestConventions(Project project) { project.getTasks().withType(Test.class, - test -> project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> { - TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class); - testRetry.getFailOnPassedAfterRetry().set(true); - testRetry.getMaxRetries().set(isCi() ? 3 : 0); - })); + test -> { + configureTests(project, test); + configureTestRetryPlugin(project, test); + }); + } + + private void configureTests(Project project, Test test) { + test.useJUnitPlatform(); + 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" + )); + if (project.hasProperty("testGroups")) { + test.systemProperty("testGroups", project.getProperties().get("testGroups")); + } + test.jvmArgs( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "-Xshare:off" + ); + } + + private void configureTestRetryPlugin(Project project, Test test) { + project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> { + TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class); + testRetry.getFailOnPassedAfterRetry().set(true); + testRetry.getMaxRetries().set(isCi() ? 3 : 0); + }); } private boolean isCi() { diff --git a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java b/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java deleted file mode 100644 index 4946191282e3..000000000000 --- a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.build.api; - -import java.io.File; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; - -import me.champeau.gradle.japicmp.JapicmpPlugin; -import me.champeau.gradle.japicmp.JapicmpTask; -import org.gradle.api.GradleException; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.plugins.JavaBasePlugin; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.jvm.tasks.Jar; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link Plugin} that applies the {@code "japicmp-gradle-plugin"} - * and create tasks for all subprojects named {@code "spring-*"}, diffing the public API one by one - * and creating the reports in {@code "build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/"}. - *

{@code "./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE"} will output the - * reports for the API diff between the baseline version and the current one for all modules. - * You can limit the report to a single module with - * {@code "./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE"}. - * - * @author Brian Clozel - */ -public class ApiDiffPlugin implements Plugin { - - private static final Logger logger = LoggerFactory.getLogger(ApiDiffPlugin.class); - - public static final String TASK_NAME = "apiDiff"; - - private static final String BASELINE_VERSION_PROPERTY = "baselineVersion"; - - private static final List PACKAGE_INCLUDES = Collections.singletonList("org.springframework.*"); - - private static final URI SPRING_MILESTONE_REPOSITORY = URI.create("https://repo.spring.io/milestone"); - - @Override - public void apply(Project project) { - if (project.hasProperty(BASELINE_VERSION_PROPERTY) && project.equals(project.getRootProject())) { - project.getPluginManager().apply(JapicmpPlugin.class); - project.getPlugins().withType(JapicmpPlugin.class, - plugin -> applyApiDiffConventions(project)); - } - } - - private void applyApiDiffConventions(Project project) { - String baselineVersion = project.property(BASELINE_VERSION_PROPERTY).toString(); - project.subprojects(subProject -> { - if (subProject.getName().startsWith("spring-")) { - createApiDiffTask(baselineVersion, subProject); - } - }); - } - - private void createApiDiffTask(String baselineVersion, Project project) { - if (isProjectEligible(project)) { - // Add Spring Milestone repository for generating diffs against previous milestones - project.getRootProject() - .getRepositories() - .maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(SPRING_MILESTONE_REPOSITORY)); - JapicmpTask apiDiff = project.getTasks().create(TASK_NAME, JapicmpTask.class); - apiDiff.setDescription("Generates an API diff report with japicmp"); - apiDiff.setGroup(JavaBasePlugin.DOCUMENTATION_GROUP); - - apiDiff.setOldClasspath(createBaselineConfiguration(baselineVersion, project)); - TaskProvider jar = project.getTasks().withType(Jar.class).named("jar"); - apiDiff.setNewArchives(project.getLayout().files(jar.get().getArchiveFile().get().getAsFile())); - apiDiff.setNewClasspath(getRuntimeClassPath(project)); - apiDiff.setPackageIncludes(PACKAGE_INCLUDES); - apiDiff.setOnlyModified(true); - apiDiff.setIgnoreMissingClasses(true); - // Ignore Kotlin metadata annotations since they contain - // illegal HTML characters and fail the report generation - apiDiff.setAnnotationExcludes(Collections.singletonList("@kotlin.Metadata")); - - apiDiff.setHtmlOutputFile(getOutputFile(baselineVersion, project)); - - apiDiff.dependsOn(project.getTasks().getByName("jar")); - } - } - - private boolean isProjectEligible(Project project) { - return project.getPlugins().hasPlugin(JavaPlugin.class) - && project.getPlugins().hasPlugin(MavenPublishPlugin.class); - } - - private Configuration createBaselineConfiguration(String baselineVersion, Project project) { - String baseline = String.join(":", - project.getGroup().toString(), project.getName(), baselineVersion); - Dependency baselineDependency = project.getDependencies().create(baseline + "@jar"); - Configuration baselineConfiguration = project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency); - try { - // eagerly resolve the baseline configuration to check whether this is a new Spring module - baselineConfiguration.resolve(); - return baselineConfiguration; - } - catch (GradleException exception) { - logger.warn("Could not resolve {} - assuming this is a new Spring module.", baseline); - } - return project.getRootProject().getConfigurations().detachedConfiguration(); - } - - private Configuration getRuntimeClassPath(Project project) { - return project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); - } - - private File getOutputFile(String baseLineVersion, Project project) { - String buildDirectoryPath = project.getRootProject() - .getLayout().getBuildDirectory().getAsFile().get().getAbsolutePath(); - Path outDir = Paths.get(buildDirectoryPath, "reports", "api-diff", - baseLineVersion + "_to_" + project.getRootProject().getVersion()); - return project.file(outDir.resolve(project.getName() + ".html").toString()); - } - -} \ No newline at end of file diff --git a/buildSrc/src/main/java/org/springframework/build/dev/LocalDevelopmentPlugin.java b/buildSrc/src/main/java/org/springframework/build/dev/LocalDevelopmentPlugin.java new file mode 100644 index 000000000000..8c8a2fd4523c --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/dev/LocalDevelopmentPlugin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.build.dev; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaBasePlugin; + +/** + * {@link Plugin} that skips documentation tasks when the {@code "-PskipDocs"} property is defined. + * + * @author Brian Clozel + */ +public class LocalDevelopmentPlugin implements Plugin { + + private static final String SKIP_DOCS_PROPERTY = "skipDocs"; + + @Override + public void apply(Project target) { + if (target.hasProperty(SKIP_DOCS_PROPERTY)) { + skipDocumentationTasks(target); + target.subprojects(this::skipDocumentationTasks); + } + } + + private void skipDocumentationTasks(Project project) { + project.afterEvaluate(p -> { + p.getTasks().matching(task -> { + return JavaBasePlugin.DOCUMENTATION_GROUP.equals(task.getGroup()) + || "distribution".equals(task.getGroup()); + }) + .forEach(task -> task.setEnabled(false)); + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentArgumentProvider.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentArgumentProvider.java new file mode 100644 index 000000000000..2a7169fd885f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentArgumentProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.build.hint; + +import java.util.Collections; + +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.process.CommandLineArgumentProvider; + +/** + * Argument provider for registering the runtime hints agent with a Java process. + */ +public interface RuntimeHintsAgentArgumentProvider extends CommandLineArgumentProvider { + + @Classpath + ConfigurableFileCollection getAgentJar(); + + @Input + SetProperty getIncludedPackages(); + + @Input + SetProperty getExcludedPackages(); + + @Override + default Iterable asArguments() { + StringBuilder packages = new StringBuilder(); + getIncludedPackages().get().forEach(packageName -> packages.append('+').append(packageName).append(',')); + getExcludedPackages().get().forEach(packageName -> packages.append('-').append(packageName).append(',')); + return Collections.singleton("-javaagent:" + getAgentJar().getSingleFile() + "=" + packages); + } +} 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 45d7aad32c20..6c7789cd02fd 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,38 +16,15 @@ package org.springframework.build.hint; -import java.util.Collections; - -import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.SetProperty; /** * Entry point to the DSL extension for the {@link RuntimeHintsAgentPlugin} Gradle plugin. * @author Brian Clozel */ -public class RuntimeHintsAgentExtension { - - private final SetProperty includedPackages; - - private final SetProperty excludedPackages; - - public RuntimeHintsAgentExtension(ObjectFactory objectFactory) { - this.includedPackages = objectFactory.setProperty(String.class).convention(Collections.singleton("org.springframework")); - this.excludedPackages = objectFactory.setProperty(String.class).convention(Collections.emptySet()); - } - - public SetProperty getIncludedPackages() { - return this.includedPackages; - } +public interface RuntimeHintsAgentExtension { - public SetProperty getExcludedPackages() { - return this.excludedPackages; - } + SetProperty getIncludedPackages(); - String asJavaAgentArgument() { - StringBuilder builder = new StringBuilder(); - this.includedPackages.get().forEach(packageName -> builder.append('+').append(packageName).append(',')); - this.excludedPackages.get().forEach(packageName -> builder.append('-').append(packageName).append(',')); - return builder.toString(); - } + SetProperty getExcludedPackages(); } 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 f8f4a513f5e2..0a77f62b6aa7 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,41 +16,85 @@ package org.springframework.build.hint; +import org.gradle.api.JavaVersion; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.Category; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.attributes.Usage; +import org.gradle.api.attributes.java.TargetJvmVersion; import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.plugins.jvm.JvmTestSuite; import org.gradle.api.tasks.testing.Test; +import org.gradle.testing.base.TestingExtension; + +import java.util.Collections; /** * {@link Plugin} that configures the {@code RuntimeHints} Java agent to test tasks. * * @author Brian Clozel + * @author Sebastien Deleuze */ public class RuntimeHintsAgentPlugin implements Plugin { public static final String RUNTIMEHINTS_TEST_TASK = "runtimeHintsTest"; private static final String EXTENSION_NAME = "runtimeHintsAgent"; + private static final String CONFIGURATION_NAME = "testRuntimeHintsAgentJar"; @Override public void apply(Project project) { project.getPlugins().withType(JavaPlugin.class, javaPlugin -> { - RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME, - RuntimeHintsAgentExtension.class, project.getObjects()); + 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 -> { test.useJUnitPlatform(options -> { options.includeTags("RuntimeHintsTests"); }); test.include("**/*Tests.class", "**/*Test.class"); test.systemProperty("java.awt.headless", "true"); - }); - project.afterEvaluate(p -> { - Jar jar = project.getRootProject().project("spring-core-test").getTasks().withType(Jar.class).named("jar").get(); - agentTest.jvmArgs("-javaagent:" + jar.getArchiveFile().get().getAsFile() + "=" + agentExtension.asJavaAgentArgument()); + 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.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test")); + }); + } + + private static RuntimeHintsAgentExtension createRuntimeHintsAgentExtension(Project project) { + RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME, RuntimeHintsAgentExtension.class); + agentExtension.getIncludedPackages().convention(Collections.singleton("org.springframework")); + agentExtension.getExcludedPackages().convention(Collections.emptySet()); + return agentExtension; + } + + private static RuntimeHintsAgentArgumentProvider createRuntimeHintsAgentArgumentProvider( + Project project, RuntimeHintsAgentExtension agentExtension) { + RuntimeHintsAgentArgumentProvider agentArgumentProvider = project.getObjects().newInstance(RuntimeHintsAgentArgumentProvider.class); + agentArgumentProvider.getAgentJar().from(createRuntimeHintsAgentConfiguration(project)); + agentArgumentProvider.getIncludedPackages().set(agentExtension.getIncludedPackages()); + agentArgumentProvider.getExcludedPackages().set(agentExtension.getExcludedPackages()); + return agentArgumentProvider; + } + + private static Configuration createRuntimeHintsAgentConfiguration(Project project) { + return project.getConfigurations().create(CONFIGURATION_NAME, configuration -> { + configuration.setCanBeConsumed(false); + configuration.setTransitive(false); // Only the built artifact is required + configuration.attributes(attributes -> { + attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.EXTERNAL)); + attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.getObjects().named(LibraryElements.class, LibraryElements.JAR)); + attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, Integer.valueOf(JavaVersion.current().getMajorVersion())); + attributes.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME)); + }); }); } } 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 d8c188f4ce26..89475866612d 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-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; -import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSetContainer; @@ -40,10 +40,10 @@ public class OptionalDependenciesPlugin implements Plugin { @Override public void apply(Project project) { - Configuration optional = project.getConfigurations().create("optional"); + Configuration optional = project.getConfigurations().create(OPTIONAL_CONFIGURATION_NAME); optional.setCanBeConsumed(false); optional.setCanBeResolved(false); - project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { + project.getPlugins().withType(JavaBasePlugin.class, (javaBasePlugin) -> { SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class) .getSourceSets(); sourceSets.all((sourceSet) -> { @@ -53,4 +53,4 @@ public void apply(Project project) { }); } -} \ No newline at end of file +} 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 de14fd691011..ed30b8609caa 100644 --- a/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java +++ b/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.build.shadow; import java.io.File; diff --git a/ci/README.adoc b/ci/README.adoc deleted file mode 100644 index 9ff0d5b1e86c..000000000000 --- a/ci/README.adoc +++ /dev/null @@ -1,57 +0,0 @@ -== Spring Framework Concourse pipeline - -The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. -The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline -for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x[Spring Framework 6.0.x]. - -=== Setting up your development environment - -If you're part of the Spring Framework project on GitHub, you can get access to CI management features. -First, you need to go to https://ci.spring.io and install the client CLI for your platform (see bottom right of the screen). - -You can then login with the instance using: - -[source] ----- -$ fly -t spring login -n spring-framework -c https://ci.spring.io ----- - -Once logged in, you should get something like: - -[source] ----- -$ fly ts -name url team expiry -spring https://ci.spring.io spring-framework Wed, 25 Mar 2020 17:45:26 UTC ----- - -=== Pipeline configuration and structure - -The build pipelines are described in `pipeline.yml` file. - -This file is listing Concourse resources, i.e. build inputs and outputs such as container images, artifact repositories, source repositories, notification services, etc. - -It also describes jobs (a job is a sequence of inputs, tasks and outputs); jobs are organized by groups. - -The `pipeline.yml` definition contains `((parameters))` which are loaded from the `parameters.yml` file or from our https://docs.cloudfoundry.org/credhub/[credhub instance]. - -You'll find in this folder the following resources: - -* `pipeline.yml` the build pipeline -* `parameters.yml` the build parameters used for the pipeline -* `images/` holds the container images definitions used in this pipeline -* `scripts/` holds the build scripts that ship within the CI container images -* `tasks` contains the task definitions used in the main `pipeline.yml` - -=== Updating the build pipeline - -Updating files on the repository is not enough to update the build pipeline, as changes need to be applied. - -The pipeline can be deployed using the following command: - -[source] ----- -$ fly -t spring set-pipeline -p spring-framework-6.0.x -c ci/pipeline.yml -l ci/parameters.yml ----- - -NOTE: This assumes that you have credhub integration configured with the appropriate secrets. diff --git a/ci/config/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 e8ff8af88b74..000000000000 --- a/ci/images/ci-image/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM ubuntu:jammy-20231004 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh - -ENV JAVA_HOME /opt/openjdk/java17 -ENV JDK17 /opt/openjdk/java17 -ENV JDK20 /opt/openjdk/java20 - -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 ab19da60336b..000000000000 --- a/ci/images/get-jdk-url.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e - -case "$1" in - java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.9+11/bellsoft-jdk17.0.9+11-linux-amd64.tar.gz" - ;; - java20) - echo "https://github.com/bell-sw/Liberica/releases/download/20.0.1+10/bellsoft-jdk20.0.1+10-linux-amd64.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 6724cd5619dc..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 java20 -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 46e3e9b070b5..000000000000 --- a/ci/parameters.yml +++ /dev/null @@ -1,11 +0,0 @@ -github-repo: "https://github.com/spring-projects/spring-framework.git" -github-repo-name: "spring-projects/spring-framework" -sonatype-staging-profile: "org.springframework" -docker-hub-organization: "springci" -artifactory-server: "https://repo.spring.io" -branch: "6.0.x" -milestone: "6.0.x" -build-name: "spring-framework" -pipeline-name: "spring-framework" -concourse-url: "https://ci.spring.io" -task-timeout: 1h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml deleted file mode 100644 index a35b12af4ae7..000000000000 --- a/ci/pipeline.yml +++ /dev/null @@ -1,403 +0,0 @@ -anchors: - git-repo-resource-source: &git-repo-resource-source - uri: ((github-repo)) - username: ((github-username)) - password: ((github-ci-release-token)) - branch: ((branch)) - gradle-enterprise-task-params: &gradle-enterprise-task-params - GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) - GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) - sonatype-task-params: &sonatype-task-params - SONATYPE_USERNAME: ((sonatype-username)) - SONATYPE_PASSWORD: ((sonatype-password)) - SONATYPE_URL: ((sonatype-url)) - SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile)) - artifactory-task-params: &artifactory-task-params - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - build-project-task-params: &build-project-task-params - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - docker-resource-source: &docker-resource-source - username: ((docker-hub-username)) - password: ((docker-hub-password)) - slack-fail-params: &slack-fail-params - text: > - :concourse-failed: - [$TEXT_FILE_CONTENT] - text_file: git-repo/build/build-scan-uri.txt - silent: true - icon_emoji: ":concourse:" - username: concourse-ci - changelog-task-params: &changelog-task-params - name: generated-changelog/tag - tag: generated-changelog/tag - body: generated-changelog/changelog.md - github-task-params: &github-task-params - GITHUB_USERNAME: ((github-username)) - GITHUB_TOKEN: ((github-ci-release-token)) - -resource_types: -- name: registry-image - type: registry-image - source: - <<: *docker-resource-source - repository: concourse/registry-image-resource - tag: 1.5.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.5.5 -- name: github-status-resource - type: registry-image - source: - <<: *docker-resource-source - repository: dpb587/github-status-resource - tag: master -- name: slack-notification - type: registry-image - source: - <<: *docker-resource-source - repository: cfcommunity/slack-notification-resource - tag: latest -resources: -- name: git-repo - type: git - icon: github - source: - <<: *git-repo-resource-source -- name: ci-images-git-repo - type: git - icon: github - source: - uri: ((github-repo)) - branch: ((branch)) - paths: ["ci/images/*"] -- name: ci-image - type: registry-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-ci - tag: ((milestone)) -- name: every-morning - type: time - icon: alarm - source: - start: 8:00 AM - stop: 9:00 AM - location: Europe/Vienna -- name: artifactory-repo - type: artifactory-resource - icon: package-variant - source: - uri: ((artifactory-server)) - username: ((artifactory-username)) - password: ((artifactory-password)) - build_name: ((build-name)) -- name: repo-status-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: build -- name: repo-status-jdk20-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: jdk20-build -- name: slack-alert - type: slack-notification - icon: slack - source: - url: ((slack-webhook-url)) -- name: github-pre-release - type: github-release - icon: briefcase-download-outline - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: true - release: false -- name: github-release - type: github-release - icon: briefcase-download - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: false -jobs: -- name: build-ci-images - plan: - - get: git-repo - - get: ci-images-git-repo - trigger: true - - task: build-ci-image - privileged: true - file: git-repo/ci/tasks/build-ci-image.yml - output_mapping: - image: ci-image - vars: - ci-image-name: ci-image - <<: *docker-resource-source - - put: ci-image - params: - image: ci-image/image.tar -- name: build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - trigger: true - - put: repo-status-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: build-project - image: ci-image - file: git-repo/ci/tasks/build-project.yml - privileged: true - timeout: ((task-timeout)) - params: - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-build - params: { state: "success", commit: "git-repo" } - - put: artifactory-repo - params: &artifactory-params - signing_key: ((signing-key)) - signing_passphrase: ((signing-passphrase)) - repo: libs-snapshot-local - folder: distribution-repository - build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}" - build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" - disable_checksum_uploads: true - threads: 8 - artifact_set: - - include: - - "/**/framework-docs-*.zip" - properties: - "zip.name": "spring-framework" - "zip.displayname": "Spring Framework" - "zip.deployed": "false" - - include: - - "/**/framework-docs-*-docs.zip" - properties: - "zip.type": "docs" - - include: - - "/**/framework-docs-*-dist.zip" - properties: - "zip.type": "dist" - - include: - - "/**/framework-docs-*-schema.zip" - properties: - "zip.type": "schema" - get_params: - threads: 8 -- name: jdk20-build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - - get: every-morning - trigger: true - - put: repo-status-jdk20-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - image: ci-image - file: git-repo/ci/tasks/check-project.yml - privileged: true - timeout: ((task-timeout)) - params: - TEST_TOOLCHAIN: 20 - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-jdk20-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk20-build - params: { state: "success", commit: "git-repo" } -- name: stage-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: M - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-milestone] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: M - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: M - <<: *github-task-params - <<: *docker-resource-source - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RC - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-rc] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RC - <<: *docker-resource-source - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RC - <<: *github-task-params - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-release] - params: - download_artifacts: true - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *docker-resource-source - <<: *artifactory-task-params - <<: *sonatype-task-params -- name: create-github-release - serial: true - plan: - - get: ci-image - - get: git-repo - - get: artifactory-repo - trigger: true - passed: [promote-release] - params: - download_artifacts: false - save_build_info: true - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RELEASE - <<: *docker-resource-source - <<: *github-task-params - - put: github-release - params: - <<: *changelog-task-params - -groups: -- name: "builds" - jobs: ["build", "jdk20-build"] -- name: "releases" - jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] -- name: "ci-images" - jobs: ["build-ci-images"] diff --git a/ci/scripts/build-pr.sh b/ci/scripts/build-pr.sh deleted file mode 100755 index 94c4e8df65b4..000000000000 --- a/ci/scripts/build-pr.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check -popd > /dev/null diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh deleted file mode 100755 index 3844d1a3ddb4..000000000000 --- a/ci/scripts/build-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository -popd > /dev/null diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh deleted file mode 100755 index 4eb582e3689e..000000000000 --- a/ci/scripts/check-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 \ - -PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check antora -popd > /dev/null diff --git a/ci/scripts/common.sh b/ci/scripts/common.sh deleted file mode 100644 index 1accaa616732..000000000000 --- a/ci/scripts/common.sh +++ /dev/null @@ -1,2 +0,0 @@ -source /opt/concourse-java.sh -setup_symlinks \ No newline at end of file diff --git a/ci/scripts/generate-changelog.sh b/ci/scripts/generate-changelog.sh deleted file mode 100755 index d3d2b97e5dba..000000000000 --- a/ci/scripts/generate-changelog.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -CONFIG_DIR=git-repo/ci/config -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) - -java -jar /github-changelog-generator.jar \ - --spring.config.location=${CONFIG_DIR}/changelog-generator.yml \ - ${version} generated-changelog/changelog.md - -echo ${version} > generated-changelog/version -echo v${version} > generated-changelog/tag diff --git a/ci/scripts/promote-version.sh b/ci/scripts/promote-version.sh deleted file mode 100755 index bd1600191a79..000000000000 --- a/ci/scripts/promote-version.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -CONFIG_DIR=git-repo/ci/config - -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) -export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } - -echo "Promotion complete" -echo $version > version/version diff --git a/ci/scripts/stage-version.sh b/ci/scripts/stage-version.sh deleted file mode 100755 index 73c57755451c..000000000000 --- a/ci/scripts/stage-version.sh +++ /dev/null @@ -1,50 +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} 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 + } +} + +javadoc { + title = "${rootProject.description} ${version} API" + options { + encoding = "UTF-8" + memberLevel = JavadocMemberLevel.PROTECTED + author = true + header = rootProject.description + use = true + overview = project.relativePath("$rootProject.rootDir/framework-docs/src/docs/api/overview.html") + 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 + } + maxMemory = "1024m" + doFirst { + classpath += files( + // ensure the javadoc process can resolve types compiled from .aj sources + project(":spring-aspects").sourceSets.main.output + ) + classpath += files(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") + } + moduleName.set("spring-framework") + outputDirectory.set(project.java.docsDir.dir("kdoc-api").get().asFile) + 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] + group = "distribution" + description = "Builds -${archiveClassifier} archive containing api and reference " + + "for deployment at https://docs.spring.io/spring-framework/docs/." + + archiveBaseName.set("spring-framework") + archiveClassifier.set("docs") + from("src/dist") { + include "changelog.txt" + } + from(javadoc) { + into "javadoc-api" + } + from(rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) { + into "kdoc-api" + } +} + +/** + * Zip all Spring Framework schemas into a single archive + */ +tasks.register('schemaZip', Zip) { + group = "distribution" + archiveBaseName.set("spring-framework") + archiveClassifier.set("schema") + description = "Builds -${archiveClassifier} archive containing all " + + "XSDs for deployment at https://springframework.org/schema." + duplicatesStrategy DuplicatesStrategy.EXCLUDE + moduleProjects.each { module -> + def Properties schemas = new Properties(); + + module.sourceSets.main.resources.find { + (it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas")) + }?.withInputStream { schemas.load(it) } + + for (def key : schemas.keySet()) { + def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1') + assert shortName != key + File xsdFile = module.sourceSets.main.resources.find { + (it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/', '\\\\'))) + } + assert xsdFile != null + into(shortName) { + from xsdFile.path + } + } + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifact docsZip + artifact schemaZip + } + } +} diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml new file mode 100644 index 000000000000..e67a80f2dee2 --- /dev/null +++ b/framework-docs/antora-playbook.yml @@ -0,0 +1,39 @@ +antora: + extensions: + - require: '@springio/antora-extensions' + root_component_name: 'framework' +site: + title: Spring Framework + url: https://docs.spring.io/spring-framework/reference + robots: allow +git: + ensure_git_suffix: false +content: + sources: + - url: https://github.com/spring-projects/spring-framework + # Refname matching: + # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ + branches: ['main', '{6..9}.+({1..9}).x'] + tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})'] + start_path: framework-docs +asciidoc: + extensions: + - '@asciidoctor/tabs' + - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/include-code-extension' + attributes: + page-stackoverflow-url: https://stackoverflow.com/questions/tagged/spring + page-pagination: '' + hide-uri-scheme: '@' + tabs-sync-option: '@' + include-java: 'example$docs-src/main/java/org/springframework/docs' +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +runtime: + log: + failure_level: warn +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip diff --git a/framework-docs/antora.yml b/framework-docs/antora.yml index 642ecd19f6cd..0601ef951ad5 100644 --- a/framework-docs/antora.yml +++ b/framework-docs/antora.yml @@ -17,16 +17,78 @@ asciidoc: # FIXME: the copyright is not removed # FIXME: The package is not renamed chomp: 'all' + fold: 'all' + table-stripes: 'odd' include-java: 'example$docs-src/main/java/org/springframework/docs' - spring-framework-main-code: 'https://github.com/spring-projects/spring-framework/tree/main' + include-kotlin: 'example$docs-src/main/kotlin/org/springframework/docs' + spring-site: 'https://spring.io' + spring-site-blog: '{spring-site}/blog' + spring-site-cve: "{spring-site}/security" + spring-site-guides: '{spring-site}/guides' + spring-site-projects: '{spring-site}/projects' + spring-site-tools: "{spring-site}/tools" + spring-org: 'spring-projects' + spring-github-org: "https://github.com/{spring-org}" + spring-framework-github: "https://github.com/{spring-org}/spring-framework" + spring-framework-code: '{spring-framework-github}/tree/main' + spring-framework-issues: '{spring-framework-github}/issues' + spring-framework-wiki: '{spring-framework-github}/wiki' + # Docs docs-site: 'https://docs.spring.io' - docs-spring: "{docs-site}/spring-framework/docs/{spring-version}" - docs-spring-framework: '{docs-site}/spring-framework/docs/{spring-version}' - api-spring-framework: '{docs-spring-framework}/javadoc-api/org/springframework' - docs-graalvm: 'https://www.graalvm.org/22.3/reference-manual' - docs-spring-boot: '{docs-site}/spring-boot/docs/current/reference' + spring-framework-docs-root: '{docs-site}/spring-framework/docs' + spring-framework-api: '{spring-framework-docs-root}/{spring-version}/javadoc-api/org/springframework' + spring-framework-api-kdoc: '{spring-framework-docs-root}/{spring-version}/kdoc-api' + spring-framework-reference: '{spring-framework-docs-root}/{spring-version}/reference' + # + # Other Spring portfolio projects + spring-boot-docs: '{docs-site}/spring-boot/docs/current/reference/html' + spring-boot-issues: '{spring-github-org}/spring-boot/issues' + # TODO add more projects / links or just build up on {docs-site}? + # TODO rename the below using new conventions docs-spring-gemfire: '{docs-site}/spring-gemfire/docs/current/reference' docs-spring-security: '{docs-site}/spring-security/reference' - gh-rsocket: 'https://github.com/rsocket' - gh-rsocket-extensions: '{gh-rsocket}/rsocket/blob/master/Extensions' - gh-rsocket-java: '{gh-rsocket}/rsocket-java{gh-rsocket}/rsocket-java' \ No newline at end of file + docs-spring-session: '{docs-site}/spring-session/reference' + # + # External projects URLs and related attributes + aspectj-site: 'https://www.eclipse.org/aspectj' + aspectj-docs: "{aspectj-site}/doc/released" + aspectj-api: "{aspectj-docs}/runtime-api" + aspectj-docs-devguide: "{aspectj-docs}/devguide" + aspectj-docs-progguide: "{aspectj-docs}/progguide" + assertj-docs: 'https://assertj.github.io/doc' + baeldung-blog: 'https://www.baeldung.com' + bean-validation-site: 'https://beanvalidation.org' + graalvm-docs: 'https://www.graalvm.org/22.3/reference-manual' + hibernate-validator-site: 'https://hibernate.org/validator/' + jackson-docs: 'https://fasterxml.github.io' + jackson-github-org: 'https://github.com/FasterXML' + java-api: 'https://docs.oracle.com/en/java/javase/17/docs/api' + java-tutorial: 'https://docs.oracle.com/javase/tutorial' + JSR: 'https://www.jcp.org/en/jsr/detail?id=' + kotlin-site: 'https://kotlinlang.org' + kotlin-docs: '{kotlin-site}/docs' + kotlin-api: '{kotlin-site}/api/latest' + kotlin-coroutines-api: '{kotlin-site}/api/kotlinx.coroutines' + kotlin-github-org: 'https://github.com/Kotlin' + kotlin-issues: 'https://youtrack.jetbrains.com/issue' + micrometer-docs: 'https://docs.micrometer.io/micrometer/reference' + micrometer-context-propagation-docs: 'https://docs.micrometer.io/context-propagation/reference' + reactive-streams-site: 'https://www.reactive-streams.org' + reactive-streams-spec: 'https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification' + reactor-github-org: 'https://github.com/reactor' + reactor-site: 'https://projectreactor.io' + rsocket-github-org: 'https://github.com/rsocket' + rsocket-java: '{rsocket-github-org}/rsocket-java' + rsocket-java-code: '{rsocket-java}/tree/master/' + rsocket-protocol-extensions: '{rsocket-github-org}/rsocket/tree/master/Extensions' + rsocket-site: 'https://rsocket.io' + rfc-site: 'https://datatracker.ietf.org/doc/html' + sockjs-client: 'https://github.com/sockjs/sockjs-client' + sockjs-protocol: 'https://github.com/sockjs/sockjs-protocol' + sockjs-protocol-site: "https://sockjs.github.io/sockjs-protocol" + stackoverflow-site: 'https://stackoverflow.com' + stackoverflow-questions: '{stackoverflow-site}/questions' + stackoverflow-spring-tag: "{stackoverflow-questions}/tagged/spring" + stackoverflow-spring-kotlin-tags: "{stackoverflow-spring-tag}+kotlin" + testcontainers-site: 'https://www.testcontainers.org' + vavr-docs: 'https://vavr-io.github.io/vavr-docs' \ No newline at end of file diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 56c68c1c7125..188b2fa255c6 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -6,51 +6,27 @@ plugins { description = "Spring Framework Docs" +apply from: "${rootDir}/gradle/ide.gradle" apply from: "${rootDir}/gradle/publications.gradle" - antora { - version = '3.2.0-alpha.2' - playbook = 'cached-antora-playbook.yml' - playbookProvider { - repository = 'spring-projects/spring-framework' - branch = 'docs-build' - path = 'lib/antora/templates/per-branch-antora-playbook.yml' - checkLocalBranch = true - } - options = ['--clean', '--stacktrace'] + options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] environment = [ - 'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691', - 'ALGOLIA_APP_ID': '244V8V9FGG', - 'ALGOLIA_INDEX_NAME': 'framework-docs' - ] - dependencies = [ - '@antora/atlas-extension': '1.0.0-alpha.1', - '@antora/collector-extension': '1.0.0-alpha.3', - '@asciidoctor/tabs': '1.0.0-beta.3', - '@opendevise/antora-release-line-extension': '1.0.0-alpha.2', - '@springio/antora-extensions': '1.3.0', - '@springio/asciidoctor-extensions': '1.0.0-alpha.9' + 'BUILD_REFNAME': 'HEAD', + 'BUILD_VERSION': project.version, ] } - tasks.named("generateAntoraYml") { asciidocAttributes = project.provider( { return ["spring-version": project.version ] } ) } -tasks.create("generateAntoraResources") { +tasks.register("generateAntoraResources") { dependsOn 'generateAntoraYml' } -// Commented out for now: -// https://github.com/spring-projects/spring-framework/issues/30481 -// tasks.named("check") { -// dependsOn 'antora' -// } - jar { enabled = false } @@ -67,166 +43,16 @@ repositories { dependencies { api(project(":spring-context")) + api(project(":spring-jdbc")) + api(project(":spring-jms")) api(project(":spring-web")) + api(project(":spring-webflux")) + + api("com.oracle.database.jdbc:ojdbc11") + api("jakarta.jms:jakarta.jms-api") api("jakarta.servlet:jakarta.servlet-api") + api("org.jetbrains.kotlin:kotlin-stdlib") implementation(project(":spring-core-test")) implementation("org.assertj:assertj-core") } - -/** - * Produce Javadoc for all Spring Framework modules in "build/docs/javadoc" - */ -task api(type: Javadoc) { - group = "Documentation" - description = "Generates aggregated Javadoc API documentation." - title = "${rootProject.description} ${version} API" - - dependsOn { - moduleProjects.collect { - it.tasks.getByName("jar") - } - } - doFirst { - classpath = files( - // ensure the javadoc process can resolve types compiled from .aj sources - project(":spring-aspects").sourceSets.main.output - ) - classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath }) - } - - options { - encoding = "UTF-8" - memberLevel = JavadocMemberLevel.PROTECTED - author = true - header = rootProject.description - use = true - overview = "framework-docs/src/docs/api/overview.html" - splitIndex = true - links(project.ext.javadocLinks) - addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint - addBooleanOption('Werror', true) // fail build on Javadoc warnings - } - source moduleProjects.collect { project -> - project.sourceSets.main.allJava - } - maxMemory = "1024m" - destinationDir = file("$buildDir/docs/javadoc") -} - -/** - * Produce KDoc for all Spring Framework modules in "build/docs/kdoc" - */ -rootProject.tasks.dokkaHtmlMultiModule.configure { - dependsOn { - tasks.getByName("api") - } - moduleName.set("spring-framework") - outputDirectory.set(project.file("$buildDir/docs/kdoc")) -} - -/** - * Zip all Java docs (javadoc & kdoc) into a single archive - */ -task docsZip(type: Zip, dependsOn: ['api', rootProject.tasks.dokkaHtmlMultiModule]) { - group = "Distribution" - description = "Builds -${archiveClassifier} archive containing api and reference " + - "for deployment at https://docs.spring.io/spring-framework/docs/." - - archiveBaseName.set("spring-framework") - archiveClassifier.set("docs") - from("src/dist") { - include "changelog.txt" - } - from (api) { - into "javadoc-api" - } - from (rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) { - into "kdoc-api" - } -} - -/** - * Zip all Spring Framework schemas into a single archive - */ -task schemaZip(type: Zip) { - group = "Distribution" - archiveBaseName.set("spring-framework") - archiveClassifier.set("schema") - description = "Builds -${archiveClassifier} archive containing all " + - "XSDs for deployment at https://springframework.org/schema." - duplicatesStrategy DuplicatesStrategy.EXCLUDE - moduleProjects.each { module -> - def Properties schemas = new Properties(); - - module.sourceSets.main.resources.find { - (it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas")) - }?.withInputStream { schemas.load(it) } - - for (def key : schemas.keySet()) { - def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1') - assert shortName != key - File xsdFile = module.sourceSets.main.resources.find { - (it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/','\\\\'))) - } - assert xsdFile != null - into (shortName) { - from xsdFile.path - } - } - } -} - -/** - * Create a distribution zip with everything: - * docs, schemas, jars, source jars, javadoc jars - */ -task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { - group = "Distribution" - archiveBaseName.set("spring-framework") - archiveClassifier.set("dist") - description = "Builds -${archiveClassifier} archive, containing all jars and docs, " + - "suitable for community download page." - - ext.baseDir = "spring-framework-${project.version}"; - - from("src/docs/dist") { - include "readme.txt" - include "license.txt" - include "notice.txt" - into "${baseDir}" - expand(copyright: new Date().format("yyyy"), version: project.version) - } - - from(zipTree(docsZip.archiveFile)) { - into "${baseDir}/docs" - } - - from(zipTree(schemaZip.archiveFile)) { - into "${baseDir}/schema" - } - - moduleProjects.each { module -> - into ("${baseDir}/libs") { - from module.jar - if (module.tasks.findByPath("sourcesJar")) { - from module.sourcesJar - } - if (module.tasks.findByPath("javadocJar")) { - from module.javadocJar - } - } - } -} - -distZip.mustRunAfter moduleProjects.check - -publishing { - publications { - mavenJava(MavenPublication) { - artifact docsZip - artifact schemaZip - artifact distZip - } - } -} \ No newline at end of file diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index c0aa14fcb7eb..b7e114263fac 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -39,8 +39,8 @@ ** xref:core/resources.adoc[] ** xref:core/validation.adoc[] *** xref:core/validation/validator.adoc[] -*** xref:core/validation/conversion.adoc[] *** xref:core/validation/beans-beans.adoc[] +*** xref:core/validation/conversion.adoc[] *** xref:core/validation/convert.adoc[] *** xref:core/validation/format.adoc[] *** xref:core/validation/format-configuring-formatting-globaldatetimeformat.adoc[] @@ -122,6 +122,7 @@ **** 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[] @@ -130,6 +131,7 @@ **** xref:testing/testcontext-framework/ctx-management/web.adoc[] **** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[] **** xref:testing/testcontext-framework/ctx-management/caching.adoc[] +**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[] **** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[] *** xref:testing/testcontext-framework/fixture-di.adoc[] *** xref:testing/testcontext-framework/web-scoped-beans.adoc[] @@ -165,6 +167,7 @@ ***** 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[] @@ -179,6 +182,7 @@ ***** 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[] @@ -266,6 +270,7 @@ ***** xref:web/webmvc/mvc-controller/ann-methods/jackson.adoc[] **** xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[] **** xref:web/webmvc/mvc-controller/ann-initbinder.adoc[] +**** xref:web/webmvc/mvc-controller/ann-validation.adoc[] **** xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc[] **** xref:web/webmvc/mvc-controller/ann-advice.adoc[] *** xref:web/webmvc-functional.adoc[] @@ -360,6 +365,7 @@ ***** xref:web/webflux/controller/ann-methods/jackson.adoc[] **** xref:web/webflux/controller/ann-modelattrib-methods.adoc[] **** xref:web/webflux/controller/ann-initbinder.adoc[] +**** xref:web/webflux/controller/ann-validation.adoc[] **** xref:web/webflux/controller/ann-exceptions.adoc[] **** xref:web/webflux/controller/ann-advice.adoc[] *** xref:web/webflux-functional.adoc[] @@ -414,6 +420,8 @@ *** xref:integration/cache/plug.adoc[] *** xref:integration/cache/specific-config.adoc[] ** xref:integration/observability.adoc[] +** xref:integration/checkpoint-restore.adoc[] +** xref:integration/cds.adoc[] ** xref:integration/appendix.adoc[] * xref:languages.adoc[] ** xref:languages/kotlin.adoc[] @@ -431,4 +439,5 @@ ** xref:languages/groovy.adoc[] ** xref:languages/dynamic.adoc[] * xref:appendix.adoc[] -* https://github.com/spring-projects/spring-framework/wiki[Wiki] \ No newline at end of file +* {spring-framework-wiki}[Wiki] + diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index c131e439ef56..eb046cc27543 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -8,7 +8,7 @@ within the core Spring Framework. [[appendix-spring-properties]] == Spring Properties -{api-spring-framework}/core/SpringProperties.html[`SpringProperties`] is a static holder +{spring-framework-api}/core/SpringProperties.html[`SpringProperties`] is a static holder for properties that control certain low-level aspects of the Spring Framework. Users can configure these properties via JVM system properties or programmatically via the `SpringProperties.setProperty(String key, String value)` method. The latter may be @@ -19,15 +19,53 @@ of the classpath -- for example, deployed within the application's JAR file. The following table lists all currently supported Spring properties. .Supported Spring Properties +[cols="1,1"] |=== | Name | Description +| `spring.aot.enabled` +| Indicates the application should run with AOT generated artifacts. See +xref:core/aot.adoc[Ahead of Time Optimizations] and +{spring-framework-api}++/aot/AotDetector.html#AOT_ENABLED++[`AotDetector`] +for details. + | `spring.beaninfo.ignore` | Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the JavaBeans `Introspector`. See -{api-spring-framework}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`] +{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`] +for details. + +| `spring.cache.reactivestreams.ignore` +| Instructs Spring's caching infrastructure to ignore the presence of Reactive Streams, +in particular Reactor's `Mono`/`Flux` in `@Cacheable` method return type declarations. See +{spring-framework-api}++/cache/interceptor/CacheAspectSupport.html#IGNORE_REACTIVESTREAMS_PROPERTY_NAME++[`CacheAspectSupport`] +for details. + +| `spring.classformat.ignore` +| Instructs Spring to ignore class format exceptions during classpath scanning, in +particular for unsupported class file versions. See +{spring-framework-api}++/context/annotation/ClassPathScanningCandidateComponentProvider.html#IGNORE_CLASSFORMAT_PROPERTY_NAME++[`ClassPathScanningCandidateComponentProvider`] +for details. + +| `spring.context.checkpoint` +| Property that specifies a common context checkpoint. See +xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic +checkpoint/restore at startup] and +{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#CHECKPOINT_PROPERTY_NAME++[`DefaultLifecycleProcessor`] +for details. + +| `spring.context.exit` +| Property for terminating the JVM when the context reaches a specific phase. See +xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic +checkpoint/restore at startup] and +{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#EXIT_PROPERTY_NAME++[`DefaultLifecycleProcessor`] for details. +| `spring.context.expression.maxLength` +| The maximum length for +xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language] +expressions used in XML bean definitions, `@Value`, etc. + | `spring.expression.compiler.mode` | The mode to use when compiling expressions for the xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language]. @@ -36,14 +74,9 @@ xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring | Instructs Spring to ignore operating system environment variables if a Spring `Environment` property -- for example, a placeholder in a configuration String -- isn't resolvable otherwise. See -{api-spring-framework}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`] +{spring-framework-api}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`] for details. -| `spring.index.ignore` -| Instructs Spring to ignore the components index located in -`META-INF/spring.components`. See xref:core/beans/classpath-scanning.adoc#beans-scanning-index[Generating an Index of Candidate Components] -. - | `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]. @@ -52,27 +85,35 @@ See the note in xref:data-access/jdbc/advanced.adoc#jdbc-batch-list[Batch Operat | Instructs Spring to ignore a default JNDI environment, as an optimization for scenarios where nothing is ever to be found for such JNDI fallback searches to begin with, avoiding the repeated JNDI lookup overhead. See -{api-spring-framework}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] +{spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] for details. | `spring.objenesis.ignore` | Instructs Spring to ignore Objenesis, not even attempting to use it. See -{api-spring-framework}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] +{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] for details. +| `spring.test.aot.processing.failOnError` +| 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. +See xref:testing/testcontext-framework/aot.adoc[Ahead of Time Support for Tests]. + | `spring.test.constructor.autowire.mode` | The default _test constructor autowire mode_ to use if `@TestConstructor` is not present -on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[Changing the default test constructor autowire mode] -. +on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[Changing the default test constructor autowire mode]. | `spring.test.context.cache.maxSize` | 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.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.enclosing.configuration` | The default _enclosing configuration inheritance mode_ to use if `@NestedTestConfiguration` is not present on a test class. See -xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[Changing the default enclosing configuration inheritance mode] -. +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[Changing the default enclosing configuration inheritance mode]. |=== diff --git a/framework-docs/modules/ROOT/pages/attributes.adoc b/framework-docs/modules/ROOT/pages/attributes.adoc deleted file mode 100644 index 177a6ae44107..000000000000 --- a/framework-docs/modules/ROOT/pages/attributes.adoc +++ /dev/null @@ -1,20 +0,0 @@ -// Spring Portfolio -:docs-site: https://docs.spring.io -:docs-spring-boot: {docs-site}/spring-boot/docs/current/reference -:docs-spring-gemfire: {docs-site}/spring-gemfire/docs/current/reference -:docs-spring-security: {docs-site}/spring-security/reference -// spring-asciidoctor-backends Settings -:chomp: default headers packages -:fold: all -// Spring Framework -:docs-spring-framework: {docs-site}/spring-framework/docs/{spring-version} -:api-spring-framework: {docs-spring-framework}/javadoc-api/org/springframework -:docs-java: {docdir}/../../main/java/org/springframework/docs -:docs-kotlin: {docdir}/../../main/kotlin/org/springframework/docs -:docs-resources: {docdir}/../../main/resources -:spring-framework-main-code: https://github.com/spring-projects/spring-framework/tree/main -// Third-party Links -:docs-graalvm: https://www.graalvm.org/22.3/reference-manual -:gh-rsocket: https://github.com/rsocket -:gh-rsocket-extensions: {gh-rsocket}/rsocket/blob/master/Extensions -:gh-rsocket-java: {gh-rsocket}/rsocket-java diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc index 8882dfd2da52..6d6f423a6f2e 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc @@ -12,5 +12,5 @@ support for new custom advice types be added without changing the core framework The only constraint on a custom `Advice` type is that it must implement the `org.aopalliance.aop.Advice` marker interface. -See the {api-spring-framework}/aop/framework/adapter/package-summary.html[`org.springframework.aop.framework.adapter`] +See the {spring-framework-api}/aop/framework/adapter/package-summary.html[`org.springframework.aop.framework.adapter`] javadoc for further information. diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc index 6927d1542739..8d79b35bbb0e 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc @@ -291,6 +291,8 @@ to consider: * `final` classes cannot be proxied, because they cannot be extended. * `final` methods cannot be advised, because they cannot be overridden. * `private` methods cannot be advised, because they cannot be overridden. +* Methods that are not visible, typically package private methods in a parent class +from a different package, cannot be advised because they are effectively private. NOTE: There is no need to add CGLIB to your classpath. CGLIB is repackaged and included in the `spring-core` JAR. In other words, CGLIB-based AOP works "out of the box", as do diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc index fdb41e7b4d18..5b891654715f 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc @@ -119,7 +119,7 @@ The following listing shows an example configuration: Note that the target object (`businessObjectTarget` in the preceding example) must be a prototype. This lets the `PoolingTargetSource` implementation create new instances -of the target to grow the pool as necessary. See the {api-spring-framework}/aop/target/AbstractPoolingTargetSource.html[javadoc of +of the target to grow the pool as necessary. See the {spring-framework-api}/aop/target/AbstractPoolingTargetSource.html[javadoc of `AbstractPoolingTargetSource`] and the concrete subclass you wish to use for information about its properties. `maxSize` is the most basic and is always guaranteed to be present. @@ -168,7 +168,7 @@ Kotlin:: ====== NOTE: Pooling stateless service objects is not usually necessary. We do not believe it should -be the default choice, as most stateless objects are naturally thread safe, and instance +be the default choice, as most stateless objects are naturally thread-safe, and instance pooling is problematic if resources are cached. Simpler pooling is available by using auto-proxying. You can set the `TargetSource` implementations @@ -221,8 +221,8 @@ NOTE: `ThreadLocal` instances come with serious issues (potentially resulting in incorrectly using them in multi-threaded and multi-classloader environments. You should always consider wrapping a `ThreadLocal` in some other class and never directly use the `ThreadLocal` itself (except in the wrapper class). Also, you should -always remember to correctly set and unset (where the latter simply involves a call to -`ThreadLocal.set(null)`) the resource local to the thread. Unsetting should be done in +always remember to correctly set and unset (where the latter involves a call to +`ThreadLocal.remove()`) the resource local to the thread. Unsetting should be done in any case, since not unsetting it might result in problematic behavior. Spring's `ThreadLocal` support does this for you and should always be considered in favor of using `ThreadLocal` instances without other proper handling code. diff --git a/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc b/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc index 1a243d51c361..28664394da12 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc @@ -52,7 +52,7 @@ Kotlin:: ---- ====== -See the {api-spring-framework}/aop/aspectj/annotation/AspectJProxyFactory.html[javadoc] for more information. +See the {spring-framework-api}/aop/aspectj/annotation/AspectJProxyFactory.html[javadoc] for more information. diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc index 952aca1f76c3..4380293f2f1b 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc @@ -4,7 +4,7 @@ @AspectJ refers to a style of declaring aspects as regular Java classes annotated with annotations. The @AspectJ style was introduced by the -https://www.eclipse.org/aspectj[AspectJ project] as part of the AspectJ 5 release. Spring +{aspectj-site}[AspectJ project] as part of the AspectJ 5 release. Spring interprets the same annotations as AspectJ 5, using a library supplied by AspectJ for pointcut parsing and matching. The AOP runtime is still pure Spring AOP, though, and there is no dependency on the AspectJ compiler or weaver. diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc index a505001c8144..55c3b9146650 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc @@ -482,7 +482,7 @@ The `JoinPoint` interface provides a number of useful methods: * `getSignature()`: Returns a description of the method that is being advised. * `toString()`: Prints a useful description of the method being advised. -See the https://www.eclipse.org/aspectj/doc/released/runtime-api/org/aspectj/lang/JoinPoint.html[javadoc] for more detail. +See the {aspectj-api}/org/aspectj/lang/JoinPoint.html[javadoc] for more detail. [[aop-ataspectj-advice-params-passing]] === Passing Parameters to Advice @@ -728,14 +728,9 @@ of determining parameter names, an exception will be thrown. `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+. -`LocalVariableTableParameterNameDiscoverer` :: Analyzes the local variable table available - in the byte code of the advice class to determine parameter names from debug information. - Requires that code be compiled with debug symbols (`-g:vars` at a minimum). Deprecated - as of Spring Framework 6.0 for removal in Spring Framework 6.1 in favor of compiling - code with `-parameters`. Not supported in a GraalVM native image. `AspectJAdviceParameterNameDiscoverer` :: Deduces parameter names from the pointcut expression, `returning`, and `throwing` clauses. See the - {api-spring-framework}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc] + {spring-framework-api}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc] for details on the algorithm used. [[aop-ataspectj-advice-params-names-explicit]] diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc index 34386f80393d..3b1ef29d767a 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc @@ -36,9 +36,9 @@ Kotlin:: The pointcut expression that forms the value of the `@Pointcut` annotation is a regular AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see -the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ +the {aspectj-docs-progguide}/index.html[AspectJ Programming Guide] (and, for extensions, the -https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 +{aspectj-docs}/adk15notebook/index.html[AspectJ 5 Developer's Notebook]) or one of the books on AspectJ (such as _Eclipse AspectJ_, by Colyer et al., or _AspectJ in Action_, by Ramnivas Laddad). @@ -392,7 +392,7 @@ method that takes no parameters, whereas `(..)` matches any number (zero or more The `({asterisk})` pattern matches a method that takes one parameter of any type. `(*,String)` matches a method that takes two parameters. The first can be of any type, while the second must be a `String`. Consult the -https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html[Language +{aspectj-docs-progguide}/semantics-pointcuts.html[Language Semantics] section of the AspectJ Programming Guide for more information. The following examples show some common pointcut expressions: diff --git a/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc b/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc index 82b705f17c29..128d6cb42884 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc @@ -31,7 +31,7 @@ However, it would be even more confusing if Spring used its own terminology. the "advised object". Since Spring AOP is implemented by using runtime proxies, this object is always a proxied object. * AOP proxy: An object created by the AOP framework in order to implement the aspect - contracts (advise method executions and so on). In the Spring Framework, an AOP proxy + contracts (advice method executions and so on). In the Spring Framework, an AOP proxy is a JDK dynamic proxy or a CGLIB proxy. * Weaving: linking aspects with other application types or objects to create an advised object. This can be done at compile time (using the AspectJ compiler, for diff --git a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc b/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc index 0187cf3a59e6..44a76320f6f6 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc @@ -19,6 +19,10 @@ you can do so. However, you should consider the following issues: 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 + `--add-opens=java.base/java.lang=ALL-UNNAMED` which is not available for modules. To force the use of CGLIB proxies, set the value of the `proxy-target-class` attribute of the `` element to true, as follows: diff --git a/framework-docs/modules/ROOT/pages/core/aop/resources.adoc b/framework-docs/modules/ROOT/pages/core/aop/resources.adoc index e95cb1f99559..ab723c056921 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/resources.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/resources.adoc @@ -2,7 +2,7 @@ = Further Resources :page-section-summary-toc: 1 -More information on AspectJ can be found on the https://www.eclipse.org/aspectj[AspectJ website]. +More information on AspectJ can be found on the {aspectj-site}[AspectJ website]. _Eclipse AspectJ_ by Adrian Colyer et. al. (Addison-Wesley, 2005) provides a comprehensive introduction and reference for the AspectJ language. diff --git a/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc b/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc index bdf6d01b7076..f17d62e2f368 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc @@ -136,7 +136,7 @@ using Spring in accordance with the properties of the annotation". In this conte "initialization" refers to newly instantiated objects (for example, objects instantiated with the `new` operator) as well as to `Serializable` objects that are undergoing deserialization (for example, through -https://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html[readResolve()]). +{java-api}/java.base/java/io/Serializable.html[readResolve()]). [NOTE] ===== @@ -168,14 +168,13 @@ Kotlin:: You can find more information about the language semantics of the various pointcut types in AspectJ -https://www.eclipse.org/aspectj/doc/next/progguide/semantics-joinPoints.html[in this -appendix] of the https://www.eclipse.org/aspectj/doc/next/progguide/index.html[AspectJ -Programming Guide]. +{aspectj-docs-progguide}/semantics-joinPoints.html[in this appendix] of the +{aspectj-docs-progguide}/index.html[AspectJ Programming Guide]. ===== For this to work, the annotated types must be woven with the AspectJ weaver. You can either use a build-time Ant or Maven task to do this (see, for example, the -https://www.eclipse.org/aspectj/doc/released/devguide/antTasks.html[AspectJ Development +{aspectj-docs-devguide}/antTasks.html[AspectJ Development Environment Guide]) or load-time weaving (see xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time Weaving with AspectJ in the Spring Framework]). The `AnnotationBeanConfigurerAspect` itself needs to be configured by Spring (in order to obtain a reference to the bean factory that is to be used to configure new objects). If you @@ -399,7 +398,7 @@ The focus of this section is on configuring and using LTW in the specific contex Spring Framework. This section is not a general introduction to LTW. For full details on the specifics of LTW and configuring LTW with only AspectJ (with Spring not being involved at all), see the -https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html[LTW section of the AspectJ +{aspectj-docs-devguide}/ltw.html[LTW section of the AspectJ Development Environment Guide]. The value that the Spring Framework brings to AspectJ LTW is in enabling much @@ -421,7 +420,7 @@ who typically are in charge of the deployment configuration, such as the launch Now that the sales pitch is over, let us first walk through a quick example of AspectJ LTW that uses Spring, followed by detailed specifics about elements introduced in the example. For a complete example, see the -https://github.com/spring-projects/spring-petclinic[Petclinic sample application]. +{spring-github-org}/spring-petclinic[Petclinic sample application]. [[aop-aj-ltw-first-example]] @@ -522,8 +521,8 @@ standard AspectJ. The following example shows the `aop.xml` file: - - + + @@ -534,6 +533,11 @@ standard AspectJ. The following example shows the `aop.xml` file: ---- +NOTE: It is recommended to only weave specific classes (typically those in the +application packages, as shown in the `aop.xml` example above) in order +to avoid side effects such as AspectJ dump files and warnings. +This is also a best practice from an efficiency perspective. + Now we can move on to the Spring-specific portion of the configuration. We need to configure a `LoadTimeWeaver` (explained later). This load-time weaver is the essential component responsible for weaving the aspect configuration in one or @@ -621,7 +625,7 @@ java -javaagent:C:/projects/xyz/lib/spring-instrument.jar com.xyz.Main ---- The `-javaagent` is a flag for specifying and enabling -https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html[agents +{java-api}/java.instrument/java/lang/instrument/package-summary.html[agents to instrument programs that run on the JVM]. The Spring Framework ships with such an agent, the `InstrumentationSavingAgent`, which is packaged in the `spring-instrument.jar` that was supplied as the value of the `-javaagent` argument in @@ -715,13 +719,32 @@ Furthermore, the compiled aspect classes need to be available on the classpath. [[aop-aj-ltw-aop_dot_xml]] -=== 'META-INF/aop.xml' +=== `META-INF/aop.xml` The AspectJ LTW infrastructure is configured by using one or more `META-INF/aop.xml` files that are on the Java classpath (either directly or, more typically, in jar files). +For example: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + + + + + +---- + +NOTE: It is recommended to only weave specific classes (typically those in the +application packages, as shown in the `aop.xml` example above) in order +to avoid side effects such as AspectJ dump files and warnings. +This is also a best practice from an efficiency perspective. The structure and contents of this file is detailed in the LTW part of the -https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html[AspectJ reference +{aspectj-docs-devguide}/ltw-configuration.html[AspectJ reference documentation]. Because the `aop.xml` file is 100% AspectJ, we do not describe it further here. diff --git a/framework-docs/modules/ROOT/pages/core/aot.adoc b/framework-docs/modules/ROOT/pages/core/aot.adoc index c7fce039272d..9446fdc24c0f 100644 --- a/framework-docs/modules/ROOT/pages/core/aot.adoc +++ b/framework-docs/modules/ROOT/pages/core/aot.adoc @@ -15,10 +15,13 @@ Applying such optimizations early implies the following restrictions: * The classpath is fixed and fully defined at build time. * The beans defined in your application cannot change at runtime, meaning: -** `@Profile`, in particular profile-specific configuration needs to be chosen at build time. +** `@Profile`, in particular profile-specific configuration, needs to be chosen at build time and is automatically enabled at runtime when AOT is enabled. ** `Environment` properties that impact the presence of a bean (`@Conditional`) are only considered at build time. -* Bean definitions with instance suppliers (lambdas or method references) cannot be transformed ahead-of-time (see related https://github.com/spring-projects/spring-framework/issues/29555[spring-framework#29555] issue). -* Make sure that the bean type is as precise as possible. +* Bean definitions with instance suppliers (lambdas or method references) cannot be transformed ahead-of-time. +* Beans registered as singletons (using `registerSingleton`, typically from +`ConfigurableListableBeanFactory`) cannot be transformed ahead-of-time either. +* As we cannot rely on the instance, make sure that the bean type is as precise as +possible. TIP: See also the xref:core/aot.adoc#aot.bestpractices[] section. @@ -27,7 +30,7 @@ A Spring AOT processed application typically generates: * Java source code * Bytecode (usually for dynamic proxies) -* {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] for the use of reflection, resource loading, serialization, and JDK proxies. +* {spring-framework-api}/aot/hint/RuntimeHints.html[`RuntimeHints`] for the use of reflection, resource loading, serialization, and JDK proxies NOTE: At the moment, AOT is focused on allowing Spring applications to be deployed as native images using GraalVM. We intend to support more JVM-based use cases in future generations. @@ -35,7 +38,7 @@ We intend to support more JVM-based use cases in future generations. [[aot.basics]] == AOT engine overview -The entry point of the AOT engine for processing an `ApplicationContext` arrangement is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {api-spring-framework}/aot/generate/GenerationContext.html[`GenerationContext`]: +The entry point of the AOT engine for processing an `ApplicationContext` is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {spring-framework-api}/aot/generate/GenerationContext.html[`GenerationContext`]: * Refresh an `ApplicationContext` for AOT processing. Contrary to a traditional refresh, this version only creates bean definitions, not bean instances. * Invoke the available `BeanFactoryInitializationAotProcessor` implementations and apply their contributions against the `GenerationContext`. @@ -67,7 +70,14 @@ include-code::./AotProcessingSample[tag=aotcontext] In this mode, xref:core/beans/factory-extension.adoc#beans-factory-extension-factory-postprocessors[`BeanFactoryPostProcessor` implementations] are invoked as usual. This includes configuration class parsing, import selectors, classpath scanning, etc. Such steps make sure that the `BeanRegistry` contains the relevant bean definitions for the application. -If bean definitions are guarded by conditions (such as `@Profile`), these are discarded at this stage. +If bean definitions are guarded by conditions (such as `@Profile`), these are evaluated, +and bean definitions that don't match their conditions are discarded at this stage. + +If custom code needs to register extra beans programmatically, make sure that custom +registration code uses `BeanDefinitionRegistry` instead of `BeanFactory` as only bean +definitions are taken into account. A good pattern is to implement +`ImportBeanDefinitionRegistrar` and register it via an `@Import` on one of your +configuration classes. Because this mode does not actually create bean instances, `BeanPostProcessor` implementations are not invoked, except for specific variants that are relevant for AOT processing. These are: @@ -81,15 +91,15 @@ Once this part completes, the `BeanFactory` contains the bean definitions that a [[aot.bean-factory-initialization-contributions]] == Bean Factory Initialization AOT Contributions -Components that want to participate in this step can implement the {api-spring-framework}/beans/factory/aot/BeanFactoryInitializationAotProcessor.html[`BeanFactoryInitializationAotProcessor`] interface. +Components that want to participate in this step can implement the {spring-framework-api}/beans/factory/aot/BeanFactoryInitializationAotProcessor.html[`BeanFactoryInitializationAotProcessor`] interface. Each implementation can return an AOT contribution, based on the state of the bean factory. -An AOT contribution is a component that contributes generated code that reproduces a particular behavior. +An AOT contribution is a component that contributes generated code which reproduces a particular behavior. It can also contribute `RuntimeHints` to indicate the need for reflection, resource loading, serialization, or JDK proxies. -A `BeanFactoryInitializationAotProcessor` implementation can be registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface. +A `BeanFactoryInitializationAotProcessor` implementation can be registered in `META-INF/spring/aot.factories` with a key equal to the fully-qualified name of the interface. -A `BeanFactoryInitializationAotProcessor` can also be implemented directly by a bean. +The `BeanFactoryInitializationAotProcessor` interface can also be implemented directly by a bean. In this mode, the bean provides an AOT contribution equivalent to the feature it provides with a regular runtime. Consequently, such a bean is automatically excluded from the AOT-optimized context. @@ -111,7 +121,7 @@ This interface is used as follows: * Implemented by a `BeanPostProcessor` bean, to replace its runtime behavior. For instance xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[`AutowiredAnnotationBeanPostProcessor`] implements this interface to generate code that injects members annotated with `@Autowired`. -* Implemented by a type registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface. +* Implemented by a type registered in `META-INF/spring/aot.factories` with a key equal to the fully-qualified name of the interface. Typically used when the bean definition needs to be tuned for specific features of the core framework. [NOTE] @@ -142,8 +152,23 @@ Java:: } ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @Configuration(proxyBeanMethods = false) + class DataSourceConfiguration { + + @Bean + fun dataSource() = SimpleDataSource() + + } +---- ====== +WARNING: Kotlin class names with backticks that use invalid Java identifiers (not starting with a letter, containing spaces, etc.) are not supported. + Since there isn't any particular condition on this class, `dataSourceConfiguration` and `dataSource` are identified as candidates. The AOT engine will convert the configuration class above to code similar to the following: @@ -156,6 +181,7 @@ Java:: /** * Bean definitions for {@link DataSourceConfiguration} */ + @Generated public class DataSourceConfiguration__BeanDefinitions { /** * Get the bean definition for 'dataSourceConfiguration' @@ -190,11 +216,25 @@ Java:: NOTE: The exact code generated may differ depending on the exact nature of your bean definitions. +TIP: Each generated class is annotated with `org.springframework.aot.generate.Generated` to +identify them if they need to be excluded, for instance by static analysis tools. + The generated code above creates bean definitions equivalent to the `@Configuration` class, but in a direct way and without the use of reflection if at all possible. There is a bean definition for `dataSourceConfiguration` and one for `dataSourceBean`. When a `datasource` instance is required, a `BeanInstanceSupplier` is called. This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean. +[[aot.running]] +== Running with AOT optimizations + +AOT is a mandatory step to transform a Spring application to a native executable, so it +is automatically enabled when running in this mode. It is possible to use those optimizations +on the JVM by setting the `spring.aot.enabled` System property to `true`. + +NOTE: When AOT optimizations are included, some decisions that have been taken at build-time +are hard-coded in the application setup. For instance, profiles that have been enabled at +build-time are automatically enabled at runtime as well. + [[aot.bestpractices]] == Best Practices @@ -203,11 +243,33 @@ However, keep in mind that some optimizations are made at build time based on a This section lists the best practices that make sure your application is ready for AOT. +[[aot.bestpractices.bean-registration]] +== Programmatic bean registration + +The AOT engine takes care of the `@Configuration` model and any callback that might be +invoked as part of processing your configuration. If you need to register additional +beans programmatically, make sure to use a `BeanDefinitionRegistry` to register +bean definitions. + +This can typically be done via a `BeanDefinitionRegistryPostProcessor`. Note that, if it +is registered itself as a bean, it will be invoked again at runtime unless you make +sure to implement `BeanFactoryInitializationAotProcessor` as well. A more idiomatic +way is to implement `ImportBeanDefinitionRegistrar` and register it using `@Import` on +one of your configuration classes. This invokes your custom code as part of configuration +class parsing. + +If you declare additional beans programmatically using a different callback, they are +likely not going to be handled by the AOT engine, and therefore no hints are going to be +generated for them. Depending on the environment, those beans may not be registered at +all. For instance, classpath scanning does not work in a native image as there is no +notion of a classpath. For cases like this, it is crucial that the scanning happens at +build time. + [[aot.bestpractices.bean-type]] === Expose The Most Precise Bean Type While your application may interact with an interface that a bean implements, it is still very important to declare the most precise type. -The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members, or lifecycle callback methods. +The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members or lifecycle callback methods. For `@Configuration` classes, make sure that the return type of the factory `@Bean` method is as precise as possible. Consider the following example: @@ -256,6 +318,40 @@ Java:: If you are registering bean definitions programmatically, consider using `RootBeanBefinition` as it allows to specify a `ResolvableType` that handles generics. +[[aot.bestpractices.constructors]] +=== Avoid Multiple Constructors + +The container is able to choose the most appropriate constructor to use based on several candidates. +However, this is not a best practice and flagging the preferred constructor with `@Autowired` if necessary is preferred. + +In case you are working on a code base that you 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.comlext-data-structure]] +=== Avoid Complex Data Structure 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 structure, decomposing to simple types or referring to a bean that is built as such is recommended. + +As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`. +To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key. + +[[aot.bestpractices.custom-arguments]] +=== Avoid Creating Bean with Custom Arguments + +Spring AOT detects what needs to be done to create a bean and translates that in generated code using an instance supplier. +The container also supports creating a bean with {spring-framework-api}++/beans/factory/BeanFactory.html#getBean(java.lang.String,java.lang.Object...)++[custom arguments] that leads to several issues with AOT: + +. The custom arguments require dynamic introspection of a matching constructor or factory method. +Those arguments cannot be detected by AOT, so the necessary reflection hints will have to be provided manually. +. By-passing the instance supplier means that all other optimizations after creation are skipped as well. +For instance, autowiring on fields and methods will be skipped as they are handled in the instance supplier. + +Rather than having prototype-scoped beans created with custom arguments, we recommend a manual factory pattern where a bean is responsible for the creation of the instance. + [[aot.bestpractices.factory-bean]] === FactoryBean @@ -272,7 +368,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- public class ClientFactoryBean implements FactoryBean { - + // ... } ---- ====== @@ -369,10 +465,10 @@ Java:: Running an application as a native image requires additional information compared to a regular JVM runtime. For instance, GraalVM needs to know ahead of time if a component uses reflection. -Similarly, classpath resources are not shipped in a native image unless specified explicitly. +Similarly, classpath resources are not included in a native image unless specified explicitly. Consequently, if the application needs to load a resource, it must be referenced from the corresponding GraalVM native image configuration file. -The {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime. +The {spring-framework-api}/aot/hint/RuntimeHints.html[`RuntimeHints`] API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime. The following example makes sure that `config/app.properties` can be loaded from the classpath at runtime within a native image: [tabs] @@ -404,16 +500,16 @@ include-code::./SpellCheckService[] If at all possible, `@ImportRuntimeHints` should be used as close as possible to the component that requires the hints. This way, if the component is not contributed to the `BeanFactory`, the hints won't be contributed either. -It is also possible to register an implementation statically by adding an entry in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the `RuntimeHintsRegistrar` interface. +It is also possible to register an implementation statically by adding an entry in `META-INF/spring/aot.factories` with a key equal to the fully-qualified name of the `RuntimeHintsRegistrar` interface. [[aot.hints.reflective]] === `@Reflective` -{api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element. +{spring-framework-api}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element. For instance, `@EventListener` is meta-annotated with `@Reflective` since the underlying implementation invokes the annotated method using reflection. -By default, only Spring beans are considered and an invocation hint is registered for the annotated element. +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. @@ -424,7 +520,7 @@ If components other than Spring beans need to be processed, a `BeanFactoryInitia [[aot.hints.register-reflection-for-binding]] === `@RegisterReflectionForBinding` -{api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a specialization of `@Reflective` that registers the need for serializing arbitrary types. +{spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a specialization of `@Reflective` that registers the need for serializing arbitrary types. A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body. `@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. @@ -460,7 +556,7 @@ include-code::./SpellCheckServiceTests[tag=hintspredicates] With `RuntimeHintsPredicates`, we can check for reflection, resource, serialization, or proxy generation hints. This approach works well for unit tests but implies that the runtime behavior of a component is well known. -You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the {docs-graalvm}/native-image/metadata/AutomaticMetadataCollection/[GraalVM tracing agent]. +You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the {graalvm-docs}/native-image/metadata/AutomaticMetadataCollection/[GraalVM tracing agent]. This agent will record all relevant calls requiring GraalVM hints at runtime and write them out as JSON configuration files. For more targeted discovery and testing, Spring Framework ships a dedicated module with core AOT testing utilities, `"org.springframework:spring-core-test"`. @@ -492,4 +588,4 @@ io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldReg There are various ways to configure this Java agent in your build, so please refer to the documentation of your build tool and test execution plugin. The agent itself can be configured to instrument specific packages (by default, only `org.springframework` is instrumented). -You'll find more details in the {spring-framework-main-code}/buildSrc/README.md[Spring Framework `buildSrc` README] file. +You'll find more details in the {spring-framework-code}/buildSrc/README.md[Spring Framework `buildSrc` README] file. diff --git a/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc b/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc index a0ccd3cb33b4..5ca36a86567b 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc @@ -765,7 +765,7 @@ want to add an additional attribute to the existing bean definition element. By way of another example, suppose that you define a bean definition for a service object that (unknown to it) accesses a clustered -https://jcp.org/en/jsr/detail?id=107[JCache], and you want to ensure that the +{JSR}107[JCache], and you want to ensure that the named JCache instance is eagerly started within the surrounding cluster. The following listing shows such a definition: diff --git a/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc b/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc index 38bccafbe276..0752b210d7ac 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc @@ -66,13 +66,13 @@ developer's intent ("`inject this constant value`"), and it reads better: [[xsd-schemas-util-frfb]] ==== Setting a Bean Property or Constructor Argument from a Field Value -{api-spring-framework}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] +{spring-framework-api}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] is a `FactoryBean` that retrieves a `static` or non-static field value. It is typically used for retrieving `public` `static` `final` constants, which may then be used to set a property value or constructor argument for another bean. The following example shows how a `static` field is exposed, by using the -{api-spring-framework}/beans/factory/config/FieldRetrievingFactoryBean.html#setStaticField(java.lang.String)[`staticField`] +{spring-framework-api}/beans/factory/config/FieldRetrievingFactoryBean.html#setStaticField(java.lang.String)[`staticField`] property: [source,xml,indent=0,subs="verbatim,quotes"] @@ -109,7 +109,7 @@ to be specified for the bean reference, as the following example shows: You can also access a non-static (instance) field of another bean, as described in the API documentation for the -{api-spring-framework}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] +{spring-framework-api}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] class. Injecting enumeration values into beans as either property or constructor arguments is diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc index c369aaa71514..f364ae8ed1af 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc @@ -1,33 +1,16 @@ [[beans-annotation-config]] = Annotation-based Container Configuration -.Are annotations better than XML for configuring Spring? -**** -The introduction of annotation-based configuration raised the question of whether this -approach is "`better`" than XML. The short answer is "`it depends.`" The long answer is -that each approach has its pros and cons, and, usually, it is up to the developer to -decide which strategy suits them better. Due to the way they are defined, annotations -provide a lot of context in their declaration, leading to shorter and more concise -configuration. However, XML excels at wiring up components without touching their source -code or recompiling them. Some developers prefer having the wiring close to the source -while others argue that annotated classes are no longer POJOs and, furthermore, that the -configuration becomes decentralized and harder to control. +Spring provides comprehensive support for annotation-based configuration, operating on +metadata in the component class itself by using annotations on the relevant class, +method, or field declaration. As mentioned in +xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[Example: The `AutowiredAnnotationBeanPostProcessor`], +Spring uses `BeanPostProcessors` in conjunction with annotations to make the core IOC +container aware of specific annotations. -No matter the choice, Spring can accommodate both styles and even mix them together. -It is worth pointing out that through its xref:core/beans/java.adoc[JavaConfig] option, Spring lets -annotations be used in a non-invasive way, without touching the target components' -source code and that, in terms of tooling, all configuration styles are supported by -https://spring.io/tools[Spring Tools] for Eclipse, Visual Studio Code, and Theia. -**** - -An alternative to XML setup is provided by annotation-based configuration, which relies -on bytecode metadata for wiring up components instead of XML declarations. Instead of -using XML to describe a bean wiring, the developer moves the configuration into the -component class itself by using annotations on the relevant class, method, or field -declaration. As mentioned in xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[Example: The `AutowiredAnnotationBeanPostProcessor`], using a -`BeanPostProcessor` in conjunction with annotations is a common means of extending the -Spring IoC container. For example, the xref:core/beans/annotation-config/autowired.adoc[`@Autowired`] -annotation provides the same capabilities as described in xref:core/beans/dependencies/factory-autowire.adoc[Autowiring Collaborators] but +For example, the xref:core/beans/annotation-config/autowired.adoc[`@Autowired`] +annotation provides the same capabilities as described in +xref:core/beans/dependencies/factory-autowire.adoc[Autowiring Collaborators] but with more fine-grained control and wider applicability. In addition, Spring provides support for JSR-250 annotations, such as `@PostConstruct` and `@PreDestroy`, as well as support for JSR-330 (Dependency Injection for Java) annotations contained in the @@ -36,13 +19,16 @@ can be found in the xref:core/beans/standard-annotations.adoc[relevant section]. [NOTE] ==== -Annotation injection is performed before XML injection. Thus, the XML configuration -overrides the annotations for properties wired through both approaches. +Annotation injection is performed before external property injection. Thus, external +configuration (e.g. XML-specified bean properties) effectively overrides the annotations +for properties when wired through mixed approaches. ==== -As always, you can register the post-processors as individual bean definitions, but they -can also be implicitly registered by including the following tag in an XML-based Spring -configuration (notice the inclusion of the `context` namespace): +Technically, you can register the post-processors as individual bean definitions, but they +are implicitly registered in an `AnnotationConfigApplicationContext` already. + +In an XML-based Spring setup, you may include the following configuration tag to enable +mixing and matching with annotation-based configuration: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -62,11 +48,11 @@ configuration (notice the inclusion of the `context` namespace): The `` element implicitly registers the following post-processors: -* {api-spring-framework}/context/annotation/ConfigurationClassPostProcessor.html[`ConfigurationClassPostProcessor`] -* {api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`] -* {api-spring-framework}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`] -* {api-spring-framework}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`] -* {api-spring-framework}/context/event/EventListenerMethodProcessor.html[`EventListenerMethodProcessor`] +* {spring-framework-api}/context/annotation/ConfigurationClassPostProcessor.html[`ConfigurationClassPostProcessor`] +* {spring-framework-api}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`] +* {spring-framework-api}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`] +* {spring-framework-api}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`] +* {spring-framework-api}/context/event/EventListenerMethodProcessor.html[`EventListenerMethodProcessor`] [NOTE] ==== diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc index 946fb6a317b2..34bf1e02b903 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 @@ -153,15 +153,15 @@ Letting qualifier values select against target bean names, within the type-match candidates, does not require a `@Qualifier` annotation at the injection point. If there is no other resolution indicator (such as a qualifier or a primary marker), for a non-unique dependency situation, Spring matches the injection point name -(that is, the field name or parameter name) against the target bean names and chooses the -same-named candidate, if any. +(that is, the field name or parameter name) against the target bean names and chooses +the same-named candidate, if any (either by bean name or by associated alias). + +Since version 6.1, this requires the `-parameters` Java compiler flag to be present. ==== -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 +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). diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc index 02e6a3d2ceae..fcf746542616 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 @@ -186,6 +186,15 @@ implementation type, consider declaring the most specific return type on your fa method (at least as specific as required by the injection points referring to your bean). ==== +[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. +==== + You can also instruct Spring to provide all beans of a particular type from the `ApplicationContext` by adding the `@Autowired` annotation to a field or method that expects an array of that type, as the following example shows: @@ -268,6 +277,12 @@ use the same bean class). `@Order` values may influence priorities at injection but be aware that they do not influence singleton startup order, which is an orthogonal concern determined by dependency relationships and `@DependsOn` declarations. +Note that `@Order` annotations on configuration classes just influence the evaluation +order within the overall set of configuration classes on startup. Such configuration-level +order values do not affect the contained `@Bean` methods at all. For bean-level ordering, +each `@Bean` method needs to have its own `@Order` annotation which applies within a +set of multiple matches for the specific bean type (as returned by the factory method). + Note that the standard `jakarta.annotation.Priority` annotation is not available at the `@Bean` level, since it cannot be declared on methods. Its semantics can be modeled through `@Order` values in combination with `@Primary` on a single bean for each type. diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc index 34d63d008483..0ca89cd0ab46 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc @@ -1,7 +1,7 @@ [[beans-custom-autowire-configurer]] = Using `CustomAutowireConfigurer` -{api-spring-framework}/beans/factory/annotation/CustomAutowireConfigurer.html[`CustomAutowireConfigurer`] +{spring-framework-api}/beans/factory/annotation/CustomAutowireConfigurer.html[`CustomAutowireConfigurer`] is a `BeanFactoryPostProcessor` that lets you register your own custom qualifier annotation types, even if they are not annotated with Spring's `@Qualifier` annotation. The following example shows how to use `CustomAutowireConfigurer`: diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/resource.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/resource.adoc index b3457b78833b..370471e57d0e 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 @@ -84,7 +84,7 @@ Kotlin:: NOTE: The name provided with the annotation is resolved as a bean name by the `ApplicationContext` of which the `CommonAnnotationBeanPostProcessor` is aware. The names can be resolved through JNDI if you configure Spring's -{api-spring-framework}/jndi/support/SimpleJndiBeanFactory.html[`SimpleJndiBeanFactory`] +{spring-framework-api}/jndi/support/SimpleJndiBeanFactory.html[`SimpleJndiBeanFactory`] explicitly. However, we recommend that you rely on the default behavior and use Spring's JNDI lookup capabilities to preserve the level of indirection. diff --git a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc index 57e51a6f1e9f..7102d7ada6be 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc @@ -2,36 +2,28 @@ = Container Overview The `org.springframework.context.ApplicationContext` interface represents the Spring IoC -container and is responsible for instantiating, configuring, and assembling the -beans. The container gets its instructions on what objects to -instantiate, configure, and assemble by reading configuration metadata. The -configuration metadata is represented in XML, Java annotations, or Java code. It lets -you express the objects that compose your application and the rich interdependencies -between those objects. - -Several implementations of the `ApplicationContext` interface are supplied -with Spring. In stand-alone applications, it is common to create an -instance of -{api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] -or {api-spring-framework}/context/support/FileSystemXmlApplicationContext.html[`FileSystemXmlApplicationContext`]. -While XML has been the traditional format for defining configuration metadata, you can -instruct the container to use Java annotations or code as the metadata format by -providing a small amount of XML configuration to declaratively enable support for these -additional metadata formats. +container and is responsible for instantiating, configuring, and assembling the beans. +The container gets its instructions on the components to instantiate, configure, and +assemble by reading configuration metadata. The configuration metadata can be represented +as annotated component classes, configuration classes with factory methods, or external +XML files or Groovy scripts. With either format, you may compose your application and the +rich interdependencies between those components. + +Several implementations of the `ApplicationContext` interface are part of core Spring. +In stand-alone applications, it is common to create an instance of +{spring-framework-api}/context/annotation/AnnotationConfigApplicationContext.html[`AnnotationConfigApplicationContext`] +or {spring-framework-api}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]. In most application scenarios, explicit user code is not required to instantiate one or -more instances of a Spring IoC container. For example, in a web application scenario, a -simple eight (or so) lines of boilerplate web descriptor XML in the `web.xml` file -of the application typically suffices (see +more instances of a Spring IoC container. For example, in a plain web application scenario, +a simple boilerplate web descriptor XML in the `web.xml` file of the application suffices (see xref:core/beans/context-introduction.adoc#context-create[Convenient ApplicationContext Instantiation for Web Applications]). -If you use the https://spring.io/tools[Spring Tools for Eclipse] (an Eclipse-powered -development environment), you can easily create this boilerplate configuration with a -few mouse clicks or keystrokes. +In a Spring Boot scenario, the application context is implicitly bootstrapped for you +based on common setup conventions. The following diagram shows a high-level view of how Spring works. Your application classes are combined with configuration metadata so that, after the `ApplicationContext` is -created and initialized, you have a fully configured and executable system or -application. +created and initialized, you have a fully configured and executable system or application. .The Spring IoC container image::container-magic.png[] @@ -43,33 +35,25 @@ image::container-magic.png[] As the preceding diagram shows, the Spring IoC container consumes a form of configuration metadata. This configuration metadata represents how you, as an -application developer, tell the Spring container to instantiate, configure, and assemble -the objects in your application. +application developer, tell the Spring container to instantiate, configure, +and assemble the components in your application. -Configuration metadata is traditionally supplied in a simple and intuitive XML format, -which is what most of this chapter uses to convey key concepts and features of the -Spring IoC container. - -NOTE: XML-based metadata is not the only allowed form of configuration metadata. The Spring IoC container itself is totally decoupled from the format in which this configuration metadata is actually written. These days, many developers choose -xref:core/beans/java.adoc[Java-based configuration] for their Spring applications. - -For information about using other forms of metadata with the Spring container, see: +xref:core/beans/java.adoc[Java-based configuration] for their Spring applications: * xref:core/beans/annotation-config.adoc[Annotation-based configuration]: define beans using - annotation-based configuration metadata. + annotation-based configuration metadata on your application's component classes. * xref:core/beans/java.adoc[Java-based configuration]: define beans external to your application - classes by using Java rather than XML files. To use these features, see the - {api-spring-framework}/context/annotation/Configuration.html[`@Configuration`], - {api-spring-framework}/context/annotation/Bean.html[`@Bean`], - {api-spring-framework}/context/annotation/Import.html[`@Import`], - and {api-spring-framework}/context/annotation/DependsOn.html[`@DependsOn`] annotations. + classes by using Java-based configuration classes. To use these features, see the + {spring-framework-api}/context/annotation/Configuration.html[`@Configuration`], + {spring-framework-api}/context/annotation/Bean.html[`@Bean`], + {spring-framework-api}/context/annotation/Import.html[`@Import`], + and {spring-framework-api}/context/annotation/DependsOn.html[`@DependsOn`] annotations. -Spring configuration consists of at least one and typically more than one bean -definition that the container must manage. XML-based configuration metadata configures these -beans as `` elements inside a top-level `` element. Java -configuration typically uses `@Bean`-annotated methods within a `@Configuration` class. +Spring configuration consists of at least one and typically more than one bean definition +that the container must manage. Java configuration typically uses `@Bean`-annotated +methods within a `@Configuration` class, each corresponding to one bean definition. These bean definitions correspond to the actual objects that make up your application. Typically, you define service layer objects, persistence layer objects such as @@ -79,7 +63,14 @@ Typically, one does not configure fine-grained domain objects in the container, it is usually the responsibility of repositories and business logic to create and load domain objects. -The following example shows the basic structure of XML-based configuration metadata: + + +[[beans-factory-xml]] +=== XML as an External Configuration DSL + +XML-based configuration metadata configures these beans as `` elements inside +a top-level `` element. The following example shows the basic structure of +XML-based configuration metadata: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -110,14 +101,9 @@ The value of the `id` attribute can be used to refer to collaborating objects. T for referring to collaborating objects is not shown in this example. See xref:core/beans/dependencies.adoc[Dependencies] for more information. - - -[[beans-factory-instantiation]] -== Instantiating a Container - -The location path or paths -supplied to an `ApplicationContext` constructor are resource strings that let -the container load configuration metadata from a variety of external resources, such +For instantiating a container, the location path or paths to the XML resource files +need to be supplied to a `ClassPathXmlApplicationContext` constructor that let the +container load configuration metadata from a variety of external resources, such as the local file system, the Java `CLASSPATH`, and so on. [tabs] @@ -141,7 +127,7 @@ Kotlin:: ==== After you learn about Spring's IoC container, you may want to know more about Spring's `Resource` abstraction (as described in -xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources]) +xref:core/resources.adoc[Resources]) which provides a convenient mechanism for reading an InputStream from locations defined in a URI syntax. In particular, `Resource` paths are used to construct applications contexts, as described in xref:core/resources.adoc#resources-app-ctx[Application Contexts and Resource Paths]. @@ -209,9 +195,9 @@ xref:core/beans/dependencies.adoc[Dependencies]. It can be useful to have bean definitions span multiple XML files. Often, each individual XML configuration file represents a logical layer or module in your architecture. -You can use the application context constructor to load bean definitions from all these +You can use the `ClassPathXmlApplicationContext` constructor to load bean definitions from XML fragments. This constructor takes multiple `Resource` locations, as was shown in the -xref:core/beans/basics.adoc#beans-factory-instantiation[previous section]. Alternatively, +xref:core/beans/basics.adoc#beans-factory-xml[previous section]. Alternatively, use one or more occurrences of the `` element to load bean definitions from another file or files. The following example shows how to do so: @@ -259,7 +245,7 @@ configuration features beyond plain bean definitions are available in a selectio of XML namespaces provided by Spring -- for example, the `context` and `util` namespaces. -[[groovy-bean-definition-dsl]] +[[beans-factory-groovy]] === The Groovy Bean Definition DSL As a further example for externalized configuration metadata, bean definitions can also @@ -420,4 +406,3 @@ a dependency on a specific bean through metadata (such as an autowiring annotati - 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 6c7f43351bd2..85d354a82c6e 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc @@ -27,7 +27,7 @@ use these features. == `@Component` and Further Stereotype Annotations The `@Repository` annotation is a marker for any class that fulfills the role or -stereotype of a repository (also known as Data Access Object or DAO). Among the uses +_stereotype_ of a repository (also known as Data Access Object or DAO). Among the uses of this marker is the automatic translation of exceptions, as described in xref:data-access/orm/general.adoc#orm-exception-translation[Exception Translation]. @@ -39,7 +39,7 @@ layers, respectively). Therefore, you can annotate your component classes with `@Component`, but, by annotating them with `@Repository`, `@Service`, or `@Controller` instead, your classes are more properly suited for processing by tools or associating with aspects. For example, these stereotype annotations make ideal targets for -pointcuts. `@Repository`, `@Service`, and `@Controller` can also +pointcuts. `@Repository`, `@Service`, and `@Controller` may also carry additional semantics in future releases of the Spring Framework. Thus, if you are choosing between using `@Component` or `@Service` for your service layer, `@Service` is clearly the better choice. Similarly, as stated earlier, `@Repository` is already @@ -191,7 +191,7 @@ Kotlin:: ====== For further details, see the -https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] +{spring-framework-wiki}/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] wiki page. @@ -315,7 +315,7 @@ entries in the classpath. When you build JARs with Ant, make sure that you do no activate the files-only switch of the JAR task. Also, classpath directories may not be exposed based on security policies in some environments -- for example, standalone apps on JDK 1.7.0_45 and higher (which requires 'Trusted-Library' setup in your manifests -- see -https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources). +{stackoverflow-questions}/19394570/java-jre-7u45-breaks-classloader-getresources). On JDK 9's module path (Jigsaw), Spring's classpath scanning generally works as expected. However, make sure that your component classes are exported in your `module-info` @@ -664,15 +664,36 @@ analogous to how the container selects between multiple `@Autowired` constructor == 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. By default, any -Spring stereotype annotation (`@Component`, `@Repository`, `@Service`, and -`@Controller`) that contains a name `value` thereby provides that name to the -corresponding bean definition. +generated by the `BeanNameGenerator` strategy known to that scanner. + +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`. + +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 +declare an attribute with a different name (such as `name`) and annotate that attribute +with `@AliasFor(annotation = Component.class, attribute = "value")`. See the source code +declaration of `ControllerAdvice#name()` for a concrete example. + +[WARNING] +==== +As of Spring Framework 6.1, support for convention-based stereotype names is deprecated +and will be removed in a future version of the framework. Consequently, custom stereotype +annotations must use `@AliasFor` to declare an explicit alias for the `value` attribute +in `@Component`. See the source code declaration of `Repository#value()` and +`ControllerAdvice#name()` for concrete examples. +==== -If such an annotation contains no name `value` or for any other detected component -(such as those discovered by custom filters), the default bean name generator returns -the uncapitalized non-qualified class name. For example, if the following component -classes were detected, the names would be `myMovieLister` and `movieFinderImpl`: +If an explicit bean name cannot be derived from such an annotation or for any other +detected component (such as those discovered by custom filters), the default bean name +generator returns the uncapitalized non-qualified class name. For example, if the +following component classes were detected, the names would be `myMovieLister` and +`movieFinderImpl`. [tabs] ====== @@ -722,7 +743,7 @@ Kotlin:: If you do not want to rely on the default bean-naming strategy, you can provide a custom bean-naming strategy. First, implement the -{api-spring-framework}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`] +{spring-framework-api}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`] interface, and be sure to include a default no-arg constructor. Then, provide the fully qualified class name when configuring the scanner, as the following example annotation and bean definition show. @@ -819,7 +840,7 @@ possibly also declaring a custom scoped-proxy mode. NOTE: To provide a custom strategy for scope resolution rather than relying on the annotation-based approach, you can implement the -{api-spring-framework}/context/annotation/ScopeMetadataResolver.html[`ScopeMetadataResolver`] +{spring-framework-api}/context/annotation/ScopeMetadataResolver.html[`ScopeMetadataResolver`] interface. Be sure to include a default no-arg constructor. Then you can provide the fully qualified class name when configuring the scanner, as the following example of both an annotation and a bean definition shows: @@ -989,68 +1010,4 @@ metadata is provided per-instance rather than per-class. -[[beans-scanning-index]] -== Generating an Index of Candidate Components - -While classpath scanning is very fast, it is possible to improve the startup performance -of large applications by creating a static list of candidates at compilation time. In this -mode, all modules that are targets of component scanning must use this mechanism. - -NOTE: Your existing `@ComponentScan` or `` directives must remain -unchanged to request the context to scan candidates in certain packages. When the -`ApplicationContext` detects such an index, it automatically uses it rather than scanning -the classpath. - -To generate the index, add an additional dependency to each module that contains -components that are targets for component scan directives. The following example shows -how to do so with Maven: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - org.springframework - spring-context-indexer - {spring-version} - true - - ----- - -With Gradle 4.5 and earlier, the dependency should be declared in the `compileOnly` -configuration, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - compileOnly "org.springframework:spring-context-indexer:{spring-version}" - } ----- - -With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor` -configuration, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - annotationProcessor "org.springframework:spring-context-indexer:{spring-version}" - } ----- - -The `spring-context-indexer` artifact generates a `META-INF/spring.components` file that -is included in the jar file. - -NOTE: When working with this mode in your IDE, the `spring-context-indexer` must be -registered as an annotation processor to make sure the index is up-to-date when -candidate components are updated. - -TIP: The index is enabled automatically when a `META-INF/spring.components` file is found -on the classpath. If an index is partially available for some libraries (or use cases) -but could not be built for the whole application, you can fall back to a regular classpath -arrangement (as though no index were present at all) by setting `spring.index.ignore` to -`true`, either as a JVM system property or via the -xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. - - - 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 16e9531eeaf5..775b3cbe10fc 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc @@ -1,10 +1,10 @@ [[context-introduction]] = Additional Capabilities of the `ApplicationContext` -As discussed in the xref:web/webmvc-view/mvc-xslt.adoc#mvc-view-xslt-beandefs[chapter introduction], the `org.springframework.beans.factory` +As discussed in the xref:core/beans/introduction.adoc[chapter introduction], the `org.springframework.beans.factory` package provides basic functionality for managing and manipulating beans, including in a programmatic way. The `org.springframework.context` package adds the -{api-spring-framework}/context/ApplicationContext.html[`ApplicationContext`] +{spring-framework-api}/context/ApplicationContext.html[`ApplicationContext`] interface, which extends the `BeanFactory` interface, in addition to extending other interfaces to provide additional functionality in a more application framework-oriented style. Many people use the `ApplicationContext` in a completely @@ -269,7 +269,7 @@ file format but is more flexible than the standard JDK based `ResourceBundleMessageSource` implementation. In particular, it allows for reading files from any Spring resource location (not only from the classpath) and supports hot reloading of bundle property files (while efficiently caching them in between). -See the {api-spring-framework}/context/support/ReloadableResourceBundleMessageSource.html[`ReloadableResourceBundleMessageSource`] +See the {spring-framework-api}/context/support/ReloadableResourceBundleMessageSource.html[`ReloadableResourceBundleMessageSource`] javadoc for details. @@ -478,20 +478,21 @@ Kotlin:: ---- ====== -Notice that `ApplicationListener` is generically parameterized with the type of your -custom event (`BlockedListEvent` in the preceding example). This means that the -`onApplicationEvent()` method can remain type-safe, avoiding any need for downcasting. -You can register as many event listeners as you wish, but note that, by default, event -listeners receive events synchronously. This means that the `publishEvent()` method -blocks until all listeners have finished processing the event. One advantage of this -synchronous and single-threaded approach is that, when a listener receives an event, -it operates inside the transaction context of the publisher if a transaction context -is available. If another strategy for event publication becomes necessary, e.g. -asynchronous event processing by default, see the javadoc for Spring's -{api-spring-framework}/context/event/ApplicationEventMulticaster.html[`ApplicationEventMulticaster`] interface -and {api-spring-framework}/context/event/SimpleApplicationEventMulticaster.html[`SimpleApplicationEventMulticaster`] -implementation for configuration options which can be applied to a custom -"applicationEventMulticaster" bean definition. +Notice that `ApplicationListener` is generically parameterized with the type of your custom event (`BlockedListEvent` in the preceding example). +This means that the `onApplicationEvent()` method can remain type-safe, avoiding any need for downcasting. +You can register as many event listeners as you wish, but note that, by default, event listeners receive events synchronously. +This means that the `publishEvent()` method blocks until all listeners have finished processing the event. +One advantage of this synchronous and single-threaded approach is that, when a listener receives an event, +it operates inside the transaction context of the publisher if a transaction context is available. +If another strategy for event publication becomes necessary, e.g. asynchronous event processing by default, +see the javadoc for Spring's {spring-framework-api}/context/event/ApplicationEventMulticaster.html[`ApplicationEventMulticaster`] interface +and {spring-framework-api}/context/event/SimpleApplicationEventMulticaster.html[`SimpleApplicationEventMulticaster`] implementation +for configuration options which can be applied to a custom "applicationEventMulticaster" bean definition. +In these cases, ThreadLocals and logging context are not propagated for the event processing. +See xref:integration/observability.adoc#observability.application-events[the `@EventListener` Observability section] +for more information on Observability concerns. + + The following example shows the bean definitions used to register and configure each of the classes above: @@ -528,7 +529,7 @@ notify appropriate parties. NOTE: Spring's eventing mechanism is designed for simple communication between Spring beans within the same application context. However, for more sophisticated enterprise integration needs, the separately maintained -https://projects.spring.io/spring-integration/[Spring Integration] project provides +{spring-site-projects}/spring-integration/[Spring Integration] project provides complete support for building lightweight, https://www.enterpriseintegrationpatterns.com[pattern-oriented], event-driven architectures that build upon the well-known Spring programming model. @@ -643,7 +644,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 @@ -659,8 +660,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) @@ -741,12 +742,15 @@ Be aware of the following limitations when using asynchronous events: * If an asynchronous event listener throws an `Exception`, it is not propagated to the caller. See - {api-spring-framework}/aop/interceptor/AsyncUncaughtExceptionHandler.html[`AsyncUncaughtExceptionHandler`] + {spring-framework-api}/aop/interceptor/AsyncUncaughtExceptionHandler.html[`AsyncUncaughtExceptionHandler`] for more details. * Asynchronous event listener methods cannot publish a subsequent event by returning a value. If you need to publish another event as the result of the processing, inject an - {api-spring-framework}/context/ApplicationEventPublisher.html[`ApplicationEventPublisher`] + {spring-framework-api}/context/ApplicationEventPublisher.html[`ApplicationEventPublisher`] to publish the event manually. +* ThreadLocals and logging context are not propagated by default for the event processing. + See xref:integration/observability.adoc#observability.application-events[the `@EventListener` Observability section] + for more information on Observability concerns. [[context-functionality-events-order]] @@ -1036,7 +1040,7 @@ and JMX support facilities. Application components can also interact with the ap server's JCA `WorkManager` through Spring's `TaskExecutor` abstraction. See the javadoc of the -{api-spring-framework}/jca/context/SpringContextResourceAdapter.html[`SpringContextResourceAdapter`] +{spring-framework-api}/jca/context/SpringContextResourceAdapter.html[`SpringContextResourceAdapter`] class for the configuration details involved in RAR deployment. For a simple deployment of a Spring ApplicationContext as a Jakarta EE RAR file: @@ -1046,7 +1050,7 @@ all application classes into a RAR file (which is a standard JAR file with a dif file extension). . Add all required library JARs into the root of the RAR archive. . Add a -`META-INF/ra.xml` deployment descriptor (as shown in the {api-spring-framework}/jca/context/SpringContextResourceAdapter.html[javadoc for `SpringContextResourceAdapter`]) +`META-INF/ra.xml` deployment descriptor (as shown in the {spring-framework-api}/jca/context/SpringContextResourceAdapter.html[javadoc for `SpringContextResourceAdapter`]) and the corresponding Spring XML bean definition file(s) (typically `META-INF/applicationContext.xml`). . Drop the resulting RAR file into your diff --git a/framework-docs/modules/ROOT/pages/core/beans/context-load-time-weaver.adoc b/framework-docs/modules/ROOT/pages/core/beans/context-load-time-weaver.adoc index 73579b414ded..25943592c078 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 @@ -44,7 +44,7 @@ weaver instance. This is particularly useful in combination with xref:data-access/orm/jpa.adoc[Spring's JPA support] where load-time weaving may be necessary for JPA class transformation. Consult the -{api-spring-framework}/orm/jpa/LocalContainerEntityManagerFactoryBean.html[`LocalContainerEntityManagerFactoryBean`] +{spring-framework-api}/orm/jpa/LocalContainerEntityManagerFactoryBean.html[`LocalContainerEntityManagerFactoryBean`] javadoc for more detail. For more on AspectJ load-time weaving, see xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time Weaving with AspectJ in the Spring Framework]. diff --git a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc index 2141abd706ff..56db659f5836 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc @@ -75,6 +75,31 @@ lead to concurrent access exceptions, inconsistent state in the bean container, +[[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. + +To disable bean overriding altogether, you can set the `allowBeanDefinitionOverriding` +flag to `false` on the `ApplicationContext` before it is refreshed. In such 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`. + +.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. +**** + + + [[beans-beanname]] == Naming Beans @@ -234,6 +259,10 @@ For details about the mechanism for supplying arguments to the constructor (if r and setting object instance properties after the object is constructed, see xref:core/beans/dependencies/factory-collaborators.adoc[Injecting Dependencies]. +NOTE: In the case of constructor arguments, the container can select a corresponding +constructor among several overloaded constructors. That said, to avoid ambiguities, +it is recommended to keep your constructor signatures as straightforward as possible. + [[beans-factory-class-static-factory-method]] === Instantiation with a Static Factory Method @@ -294,6 +323,24 @@ For details about the mechanism for supplying (optional) arguments to the factor and setting object instance properties after the object is returned from the factory, see xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail]. +NOTE: In the case of factory method arguments, the container can select a corresponding +method among several overloaded methods of the same name. That said, to avoid ambiguities, +it is recommended to keep your factory method signatures as straightforward as possible. + +[TIP] +==== +A typical problematic case with factory method overloading is Mockito with its many +overloads of the `mock` method. Choose the most specific variant of `mock` possible: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- +==== + [[beans-factory-class-instance-factory-method]] === Instantiation by Using an Instance Factory Method @@ -416,8 +463,8 @@ Kotlin:: ====== This approach shows that the factory bean itself can be managed and configured through -dependency injection (DI). See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail] -. +dependency injection (DI). +See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail]. NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the Spring container and that creates objects through an @@ -444,5 +491,3 @@ cases into account and returns the type of object that a `BeanFactory.getBean` c going to return for the same bean name. - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-collaborators.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-collaborators.adoc index 25bbaff2b63b..ad4358b3a03e 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 @@ -159,10 +159,12 @@ Kotlin:: ---- ====== -.[[beans-factory-ctor-arguments-type]]Constructor argument type matching --- +[discrete] +[[beans-factory-ctor-arguments-type]] +==== Constructor argument type matching + In the preceding scenario, the container can use type matching with simple types if -you explicitly specify the type of the constructor argument by using the `type` attribute, +you explicitly specify the type of the constructor argument via the `type` attribute, as the following example shows: [source,xml,indent=0,subs="verbatim,quotes"] @@ -172,10 +174,11 @@ as the following example shows: ---- --- -.[[beans-factory-ctor-arguments-index]]Constructor argument index --- +[discrete] +[[beans-factory-ctor-arguments-index]] +==== Constructor argument index + You can use the `index` attribute to specify explicitly the index of constructor arguments, as the following example shows: @@ -191,10 +194,11 @@ In addition to resolving the ambiguity of multiple simple values, specifying an resolves ambiguity where a constructor has two arguments of the same type. NOTE: The index is 0-based. --- -.[[beans-factory-ctor-arguments-name]]Constructor argument name --- +[discrete] +[[beans-factory-ctor-arguments-name]] +==== Constructor argument name + You can also use the constructor parameter name for value disambiguation, as the following example shows: @@ -207,8 +211,8 @@ example shows: ---- Keep in mind that, to make this work out of the box, your code must be compiled with the -debug flag enabled so that Spring can look up the parameter name from the constructor. -If you cannot or do not want to compile your code with the debug flag, you can use the +`-parameters` flag enabled so that Spring can look up the parameter name from the constructor. +If you cannot or do not want to compile your code with the `-parameters` flag, you can use the https://download.oracle.com/javase/8/docs/api/java/beans/ConstructorProperties.html[@ConstructorProperties] JDK annotation to explicitly name your constructor arguments. The sample class would then have to look as follows: @@ -244,7 +248,6 @@ Kotlin:: constructor(val years: Int, val ultimateAnswer: String) ---- ====== --- [[beans-setter-injection]] 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..bbc4c0ef20b7 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,14 @@ = 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 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: [source,xml,indent=0,subs="verbatim,quotes"] ---- diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc index 108202c40ac7..3738a55f8188 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc @@ -99,7 +99,7 @@ container, lets you handle this use case cleanly. **** You can read more about the motivation for Method Injection in -https://spring.io/blog/2004/08/06/method-injection/[this blog entry]. +{spring-site-blog}/2004/08/06/method-injection/[this blog entry]. **** @@ -201,7 +201,7 @@ the original class. Consider the following example: - + diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc index ae7874fa329f..ded99ad42d01 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc @@ -51,7 +51,7 @@ XML configuration: The preceding XML is more succinct. However, typos are discovered at runtime rather than design time, unless you use an IDE (such as https://www.jetbrains.com/idea/[IntelliJ -IDEA] or the https://spring.io/tools[Spring Tools for Eclipse]) +IDEA] or the {spring-site-tools}[Spring Tools for Eclipse]) that supports automatic property completion when you create bean definitions. Such IDE assistance is highly recommended. @@ -582,7 +582,7 @@ it needs to be declared in the XML file even though it is not defined in an XSD (it exists inside the Spring core). For the rare cases where the constructor argument names are not available (usually if -the bytecode was compiled without debugging information), you can use fallback to the +the bytecode was compiled without the `-parameters` flag), you can fall back to the argument indexes, as follows: [source,xml,indent=0,subs="verbatim,quotes"] diff --git a/framework-docs/modules/ROOT/pages/core/beans/environment.adoc b/framework-docs/modules/ROOT/pages/core/beans/environment.adoc index eaf43769ce49..ac8085f0e287 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/environment.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/environment.adoc @@ -1,7 +1,7 @@ [[beans-environment]] = Environment Abstraction -The {api-spring-framework}/core/env/Environment.html[`Environment`] interface +The {spring-framework-api}/core/env/Environment.html[`Environment`] interface is an abstraction integrated in the container that models two key aspects of the application environment: xref:core/beans/environment.adoc#beans-definition-profiles[profiles] and xref:core/beans/environment.adoc#beans-property-source-abstraction[properties]. @@ -118,7 +118,7 @@ situation B. We start by updating our configuration to reflect this need. [[beans-definition-profiles-java]] === Using `@Profile` -The {api-spring-framework}/context/annotation/Profile.html[`@Profile`] +The {spring-framework-api}/context/annotation/Profile.html[`@Profile`] annotation lets you indicate that a component is eligible for registration when one or more specified profiles are active. Using our preceding example, we can rewrite the `dataSource` configuration as follows: @@ -516,8 +516,8 @@ as the following example shows: [[beans-definition-profiles-default]] === Default Profile -The default profile represents the profile that is enabled by default. Consider the -following example: +The default profile represents the profile that is enabled if no profile is active. Consider +the following example: [tabs] ====== @@ -558,9 +558,9 @@ Kotlin:: ---- ====== -If no profile is active, the `dataSource` is created. You can see this -as a way to provide a default definition for one or more beans. If any -profile is enabled, the default profile does not apply. +If xref:#beans-definition-profiles-enable[no profile is active], the `dataSource` is +created. You can see this as a way to provide a default definition for one or more +beans. If any profile is enabled, the default profile does not apply. The name of the default profile is `default`. You can change the name of the default profile by using `setDefaultProfiles()` on the `Environment` or, @@ -599,17 +599,17 @@ Kotlin:: In the preceding snippet, we see a high-level way of asking Spring whether the `my-property` property is defined for the current environment. To answer this question, the `Environment` object performs -a search over a set of {api-spring-framework}/core/env/PropertySource.html[`PropertySource`] +a search over a set of {spring-framework-api}/core/env/PropertySource.html[`PropertySource`] objects. A `PropertySource` is a simple abstraction over any source of key-value pairs, and -Spring's {api-spring-framework}/core/env/StandardEnvironment.html[`StandardEnvironment`] +Spring's {spring-framework-api}/core/env/StandardEnvironment.html[`StandardEnvironment`] is configured with two PropertySource objects -- one representing the set of JVM system properties (`System.getProperties()`) and one representing the set of system environment variables (`System.getenv()`). NOTE: These default property sources are present for `StandardEnvironment`, for use in standalone -applications. {api-spring-framework}/web/context/support/StandardServletEnvironment.html[`StandardServletEnvironment`] +applications. {spring-framework-api}/web/context/support/StandardServletEnvironment.html[`StandardServletEnvironment`] is populated with additional default property sources including servlet config, servlet -context parameters, and a {api-spring-framework}/jndi/JndiPropertySource.html[`JndiPropertySource`] +context parameters, and a {spring-framework-api}/jndi/JndiPropertySource.html[`JndiPropertySource`] if JNDI is available. Concretely, when you use the `StandardEnvironment`, the call to `env.containsProperty("my-property")` @@ -663,7 +663,7 @@ Kotlin:: In the preceding code, `MyPropertySource` has been added with highest precedence in the search. If it contains a `my-property` property, the property is detected and returned, in favor of any `my-property` property in any other `PropertySource`. The -{api-spring-framework}/core/env/MutablePropertySources.html[`MutablePropertySources`] +{spring-framework-api}/core/env/MutablePropertySources.html[`MutablePropertySources`] API exposes a number of methods that allow for precise manipulation of the set of property sources. @@ -672,7 +672,7 @@ property sources. [[beans-using-propertysource]] == Using `@PropertySource` -The {api-spring-framework}/context/annotation/PropertySource.html[`@PropertySource`] +The {spring-framework-api}/context/annotation/PropertySource.html[`@PropertySource`] annotation provides a convenient and declarative mechanism for adding a `PropertySource` to Spring's `Environment`. @@ -772,11 +772,9 @@ resolved to the corresponding value. If not, then `default/path` is used as a default. If no default is specified and a property cannot be resolved, an `IllegalArgumentException` is thrown. -NOTE: The `@PropertySource` annotation is repeatable, according to Java 8 conventions. -However, all such `@PropertySource` annotations need to be declared at the same -level, either directly on the configuration class or as meta-annotations within the -same custom annotation. Mixing direct annotations and meta-annotations is not -recommended, since direct annotations effectively override meta-annotations. +NOTE: `@PropertySource` can be used as a repeatable annotation. `@PropertySource` +may also be used as a meta-annotation to create custom composed annotations with +attribute overrides. diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc index a82606dc0e9b..93e8e7a81df2 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -22,8 +22,8 @@ in which these `BeanPostProcessor` instances run by setting the `order` property You can set this property only if the `BeanPostProcessor` implements the `Ordered` interface. If you write your own `BeanPostProcessor`, you should consider implementing the `Ordered` interface, too. For further details, see the javadoc of the -{api-spring-framework}/beans/factory/config/BeanPostProcessor.html[`BeanPostProcessor`] -and {api-spring-framework}/core/Ordered.html[`Ordered`] interfaces. See also the note on +{spring-framework-api}/beans/factory/config/BeanPostProcessor.html[`BeanPostProcessor`] +and {spring-framework-api}/core/Ordered.html[`Ordered`] interfaces. See also the note on xref:core/beans/factory-extension.adoc#beans-factory-programmatically-registering-beanpostprocessors[programmatic registration of `BeanPostProcessor` instances]. [NOTE] @@ -272,8 +272,8 @@ which these `BeanFactoryPostProcessor` instances run by setting the `order` prop However, you can only set this property if the `BeanFactoryPostProcessor` implements the `Ordered` interface. If you write your own `BeanFactoryPostProcessor`, you should consider implementing the `Ordered` interface, too. See the javadoc of the -{api-spring-framework}/beans/factory/config/BeanFactoryPostProcessor.html[`BeanFactoryPostProcessor`] -and {api-spring-framework}/core/Ordered.html[`Ordered`] interfaces for more details. +{spring-framework-api}/beans/factory/config/BeanFactoryPostProcessor.html[`BeanFactoryPostProcessor`] +and {spring-framework-api}/core/Ordered.html[`Ordered`] interfaces for more details. [NOTE] ==== @@ -439,7 +439,7 @@ dataSource.url=jdbc:mysql:mydb ---- This example file can be used with a container definition that contains a bean called -`dataSource` that has `driver` and `url` properties. +`dataSource` that has `driverClassName` and `url` properties. Compound property names are also supported, as long as every component of the path except the final property being overridden is already non-null (presumably initialized diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc index c23676f938e7..7ed7cd011825 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc @@ -592,6 +592,40 @@ Kotlin:: +[[beans-factory-thread-safety]] +=== Thread Safety and Visibility + +The Spring core container publishes created singleton instances in a thread-safe manner, +guarding access through a singleton lock and guaranteeing visibility in other threads. + +As a consequence, application-provided bean classes do not have to be concerned about the +visibility of their initialization state. Regular configuration fields do not have to be +marked as `volatile` as long as they are only mutated during the initialization phase, +providing visibility guarantees similar to `final` even for setter-based configuration +state that is mutable during that initial phase. If such fields get changed after the +bean creation phase and its subsequent initial publication, they need to be declared as +`volatile` or guarded by a common lock whenever accessed. + +Note that concurrent access to such configuration state in singleton bean instances, +e.g. for controller instances or repository instances, is perfectly thread-safe after +such safe initial publication from the container side. This includes common singleton +`FactoryBean` instances which are processed within the general singleton lock as well. + +For destruction callbacks, the configuration state remains thread-safe but any runtime +state accumulated between initialization and destruction should be kept in thread-safe +structures (or in `volatile` fields for simple cases) as per common Java guidelines. + +Deeper `Lifecycle` integration as shown above involves runtime-mutable state such as +a `runnable` field which will have to be declared as `volatile`. While the common +lifecycle callbacks follow a certain order, e.g. a start callback is guaranteed to +only happen after full initialization and a stop callback only after an initial start, +there is a special case with the common stop before destroy arrangement: It is strongly +recommended that the internal state in any such bean also allows for an immediate +destroy callback without a preceding stop since this may happen during an extraordinary +shutdown after a cancelled bootstrap or in case of a stop timeout caused by another bean. + + + [[beans-factory-aware]] == `ApplicationContextAware` and `BeanNameAware` diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc index 8243d755d1e1..6049003235d4 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc @@ -51,7 +51,7 @@ The following table describes the supported scopes: NOTE: A thread scope is available but is not registered by default. For more information, see the documentation for -{api-spring-framework}/context/support/SimpleThreadScope.html[`SimpleThreadScope`]. +{spring-framework-api}/context/support/SimpleThreadScope.html[`SimpleThreadScope`]. For instructions on how to register this or any other custom scope, see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-custom-using[Using a Custom Scope]. @@ -324,7 +324,6 @@ Kotlin:: - [[beans-factory-scopes-application]] === Application Scope @@ -374,7 +373,6 @@ Kotlin:: - [[beans-factory-scopes-websocket]] === WebSocket Scope @@ -384,7 +382,6 @@ xref:web/websocket/stomp/scope.adoc[WebSocket scope] for more details. - [[beans-factory-scopes-other-injection]] === Scoped Beans as Dependencies @@ -544,6 +541,19 @@ see xref:core/aop/proxying.adoc[Proxying Mechanisms]. +[[beans-factory-scopes-injection]] +=== Injecting Request/Session References Directly + +As an alternative to factory scopes, a Spring `WebApplicationContext` also supports +the injection of `HttpServletRequest`, `HttpServletResponse`, `HttpSession`, +`WebRequest` and (if JSF is present) `FacesContext` and `ExternalContext` into +Spring-managed beans, simply through type-based autowiring next to regular injection +points for other beans. Spring generally injects proxies for such request and session +objects which has the advantage of working in singleton beans and serializable beans +as well, similar to scoped proxies for factory-scoped beans. + + + [[beans-factory-scopes-custom]] == Custom Scopes @@ -559,7 +569,7 @@ To integrate your custom scopes into the Spring container, you need to implement `org.springframework.beans.factory.config.Scope` interface, which is described in this section. For an idea of how to implement your own scopes, see the `Scope` implementations that are supplied with the Spring Framework itself and the -{api-spring-framework}/beans/factory/config/Scope.html[`Scope`] javadoc, +{spring-framework-api}/beans/factory/config/Scope.html[`Scope`] javadoc, which explains the methods you need to implement in more detail. The `Scope` interface has four methods to get objects from the scope, remove them from @@ -629,7 +639,7 @@ Kotlin:: ---- ====== -See the {api-spring-framework}/beans/factory/config/Scope.html#registerDestructionCallback[javadoc] +See the {spring-framework-api}/beans/factory/config/Scope.html#registerDestructionCallback[javadoc] or a Spring scope implementation for more information on destruction callbacks. The following method obtains the conversation identifier for the underlying scope: diff --git a/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc b/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc index 84fce22f428d..969cbed145f6 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc @@ -1,22 +1,22 @@ [[beans-introduction]] = Introduction to the Spring IoC Container and Beans -This chapter covers the Spring Framework implementation of the Inversion of Control -(IoC) principle. IoC is also known as dependency injection (DI). It is a process whereby -objects define their dependencies (that is, the other objects they work with) only through -constructor arguments, arguments to a factory method, or properties that are set on the -object instance after it is constructed or returned from a factory method. The container +This chapter covers the Spring Framework implementation of the Inversion of Control (IoC) +principle. Dependency injection (DI) is a specialized form of IoC, whereby objects define +their dependencies (that is, the other objects they work with) only through constructor +arguments, arguments to a factory method, or properties that are set on the object +instance after it is constructed or returned from a factory method. The IoC container then injects those dependencies when it creates the bean. This process is fundamentally -the inverse (hence the name, Inversion of Control) of the bean itself -controlling the instantiation or location of its dependencies by using direct -construction of classes or a mechanism such as the Service Locator pattern. +the inverse (hence the name, Inversion of Control) of the bean itself controlling the +instantiation or location of its dependencies by using direct construction of classes or +a mechanism such as the Service Locator pattern. The `org.springframework.beans` and `org.springframework.context` packages are the basis for Spring Framework's IoC container. The -{api-spring-framework}/beans/factory/BeanFactory.html[`BeanFactory`] +{spring-framework-api}/beans/factory/BeanFactory.html[`BeanFactory`] interface provides an advanced configuration mechanism capable of managing any type of object. -{api-spring-framework}/context/ApplicationContext.html[`ApplicationContext`] +{spring-framework-api}/context/ApplicationContext.html[`ApplicationContext`] is a sub-interface of `BeanFactory`. It adds: * Easier integration with Spring's AOP features diff --git a/framework-docs/modules/ROOT/pages/core/beans/java.adoc b/framework-docs/modules/ROOT/pages/core/beans/java.adoc index 9cb9f492b6e7..8f3f9f7aac0b 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java.adoc @@ -3,17 +3,5 @@ :page-section-summary-toc: 1 This section covers how to use annotations in your Java code to configure the Spring -container. It includes the following topics: - -* xref:core/beans/java/basic-concepts.adoc[Basic Concepts: `@Bean` and `@Configuration`] -* xref:core/beans/java/instantiating-container.adoc[Instantiating the Spring Container by Using `AnnotationConfigApplicationContext`] -* xref:core/beans/java/bean-annotation.adoc[Using the `@Bean` Annotation] -* xref:core/beans/java/configuration-annotation.adoc[Using the `@Configuration` annotation] -* xref:core/beans/java/composing-configuration-classes.adoc[Composing Java-based Configurations] -* xref:core/beans/environment.adoc#beans-definition-profiles[Bean Definition Profiles] -* xref:core/beans/environment.adoc#beans-property-source-abstraction[`PropertySource` Abstraction] -* xref:core/beans/environment.adoc#beans-using-propertysource[Using `@PropertySource`] -* xref:core/beans/environment.adoc#beans-placeholder-resolution-in-statements[Placeholder Resolution in Statements] - - +container. diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/basic-concepts.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/basic-concepts.adoc index f90a2752b7ea..5b9277838956 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 @@ -55,33 +55,34 @@ The preceding `AppConfig` class is equivalent to the following Spring `` ---- -.Full @Configuration vs "`lite`" @Bean mode? +.@Configuration classes with or without local calls between @Bean methods? **** -When `@Bean` methods are declared within classes that are not annotated with -`@Configuration`, they are referred to as being processed in a "`lite`" mode. Bean methods -declared in a `@Component` or even in a plain old class are considered to be "`lite`", -with a different primary purpose of the containing class and a `@Bean` method -being a sort of bonus there. For example, service components may expose management views -to the container through an additional `@Bean` method on each applicable component class. -In such scenarios, `@Bean` methods are a general-purpose factory method mechanism. - -Unlike full `@Configuration`, lite `@Bean` methods cannot declare inter-bean dependencies. -Instead, they operate on their containing component's internal state and, optionally, on -arguments that they may declare. Such a `@Bean` method should therefore not invoke other -`@Bean` methods. Each such method is literally only a factory method for a particular -bean reference, without any special runtime semantics. The positive side-effect here is -that no CGLIB subclassing has to be applied at runtime, so there are no limitations in -terms of class design (that is, the containing class may be `final` and so forth). - In common scenarios, `@Bean` methods are to be declared within `@Configuration` classes, -ensuring that "`full`" mode is always used and that cross-method references therefore -get redirected to the container's lifecycle management. This prevents the same -`@Bean` method from accidentally being invoked through a regular Java call, which helps -to reduce subtle bugs that can be hard to track down when operating in "`lite`" mode. +ensuring that full configuration class processing applies and that cross-method +references therefore get redirected to the container's lifecycle management. +This prevents the same `@Bean` method from accidentally being invoked through a regular +Java method call, which helps to reduce subtle bugs that can be hard to track down. + +When `@Bean` methods are declared within classes that are not annotated with +`@Configuration` - or when `@Configuration(proxyBeanMethods=false)` is declared -, +they are referred to as being processed in a "lite" mode. In such scenarios, +`@Bean` methods are effectively a general-purpose factory method mechanism without +special runtime processing (that is, without generating a CGLIB subclass for it). +A custom Java call to such a method will not get intercepted by the container and +therefore behaves just like a regular method call, creating a new instance every time +rather than reusing an existing singleton (or scoped) instance for the given bean. + +As a consequence, `@Bean` methods on classes without runtime proxying are not meant to +declare inter-bean dependencies at all. Instead, they are expected to operate on their +containing component's fields and, optionally, on arguments that a factory method may +declare in order to receive autowired collaborators. Such a `@Bean` method therefore +never needs to invoke other `@Bean` methods; every such call can be expressed through +a factory method argument instead. The positive side-effect here is that no CGLIB +subclassing has to be applied at runtime, reducing the overhead and the footprint. **** The `@Bean` and `@Configuration` annotations are discussed in depth in the following sections. -First, however, we cover the various ways of creating a spring container by using +First, however, we cover the various ways of creating a Spring container by using Java-based configuration. diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/bean-annotation.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/bean-annotation.adoc index dd35cefa9d33..4e089707ac84 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 @@ -552,7 +552,7 @@ Sometimes, it is helpful to provide a more detailed textual description of a bea be particularly useful when beans are exposed (perhaps through JMX) for monitoring purposes. To add a description to a `@Bean`, you can use the -{api-spring-framework}/context/annotation/Description.html[`@Description`] +{spring-framework-api}/context/annotation/Description.html[`@Description`] annotation, as the following example shows: [tabs] diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc index 638c8736b34f..d09faa734e08 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 @@ -116,7 +116,7 @@ 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] @@ -225,7 +225,7 @@ Also, be particularly careful with `BeanPostProcessor` and `BeanFactoryPostProce through `@Bean`. Those should usually be declared as `static @Bean` methods, not triggering the instantiation of their containing configuration class. Otherwise, `@Autowired` and `@Value` may not work on the configuration class itself, since it is possible to create it as a bean instance earlier than -{api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`]. +{spring-framework-api}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`]. ==== The following example shows how one bean can be autowired to another bean: @@ -331,14 +331,16 @@ TIP: Constructor injection in `@Configuration` classes is only supported as of S Framework 4.3. Note also that there is no need to specify `@Autowired` if the target bean defines only one constructor. -.[[beans-java-injecting-imported-beans-fq]]Fully-qualifying imported beans for ease of navigation --- +[discrete] +[[beans-java-injecting-imported-beans-fq]] +==== Fully-qualifying imported beans for ease of navigation + In the preceding scenario, using `@Autowired` works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at `ServiceConfig`, how do you know exactly where the `@Autowired AccountRepository` bean is declared? It is not explicit in the code, and this may be just fine. Remember that the -https://spring.io/tools[Spring Tools for Eclipse] provides tooling that +{spring-site-tools}[Spring Tools for Eclipse] provides tooling that can render graphs showing how everything is wired, which may be all you need. Also, your Java IDE can easily find all declarations and uses of the `AccountRepository` type and quickly show you the location of `@Bean` methods that return that type. @@ -501,7 +503,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. --- TIP: If you want to influence the startup creation order of certain beans, consider declaring some of them as `@Lazy` (for creation on first access instead of on startup) @@ -519,7 +520,7 @@ profile has been enabled in the Spring `Environment` (see xref:core/beans/enviro for details). The `@Profile` annotation is actually implemented by using a much more flexible annotation -called {api-spring-framework}/context/annotation/Conditional.html[`@Conditional`]. +called {spring-framework-api}/context/annotation/Conditional.html[`@Conditional`]. The `@Conditional` annotation indicates specific `org.springframework.context.annotation.Condition` implementations that should be consulted before a `@Bean` is registered. @@ -540,7 +541,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; } } @@ -559,7 +560,7 @@ Kotlin:: val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name) if (attrs != null) { for (value in attrs["value"]!!) { - if (context.environment.acceptsProfiles(Profiles.of(*value as Array))) { + if (context.environment.matchesProfiles(*value as Array)) { return true } } @@ -570,7 +571,7 @@ Kotlin:: ---- ====== -See the {api-spring-framework}/context/annotation/Conditional.html[`@Conditional`] +See the {spring-framework-api}/context/annotation/Conditional.html[`@Conditional`] javadoc for more detail. @@ -594,16 +595,18 @@ 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] ====== @@ -624,7 +627,7 @@ Java:: @Bean public TransferService transferService() { - return new TransferService(accountRepository()); + return new TransferServiceImpl(accountRepository()); } } ---- @@ -657,6 +660,7 @@ The following example shows part of a sample `system-test-config.xml` file: + @@ -703,20 +707,20 @@ Kotlin:: ---- ====== - -NOTE: In `system-test-config.xml` file, the `AppConfig` `` does not declare an `id` -element. While it would be acceptable to do so, it is unnecessary, given that no other bean +NOTE: In the `system-test-config.xml` file, the `AppConfig` `` does not declare an `id` +attribute. While it would be acceptable to do so, it is unnecessary, given that no other bean ever refers to it, and it is unlikely to be explicitly fetched from the container by name. Similarly, the `DataSource` bean is only ever autowired by type, so an explicit bean `id` is not strictly required. --- -.[[beans-java-combining-xml-centric-component-scan]] Using to pick up `@Configuration` classes --- +[discrete] +[[beans-java-combining-xml-centric-component-scan]] +==== Using to pick up `@Configuration` classes + Because `@Configuration` is meta-annotated with `@Component`, `@Configuration`-annotated classes are automatically candidates for component scanning. Using the same scenario as -described in the previous example, we can redefine `system-test-config.xml` to take advantage of component-scanning. -Note that, in this case, we need not explicitly declare +described in the previous example, we can redefine `system-test-config.xml` to take +advantage of component-scanning. Note that, in this case, we need not explicitly declare ``, because `` enables the same functionality. @@ -727,6 +731,7 @@ The following example shows the modified `system-test-config.xml` file: + @@ -736,19 +741,17 @@ 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] ====== @@ -773,6 +776,17 @@ 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); + } + } ---- @@ -797,21 +811,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= @@ -844,5 +869,3 @@ Kotlin:: ---- ====== - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/instantiating-container.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/instantiating-container.adoc index 3a1d9b9176cc..137bfe28398b 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 @@ -278,7 +278,7 @@ init-param): NOTE: For programmatic use cases, a `GenericWebApplicationContext` can be used as an alternative to `AnnotationConfigWebApplicationContext`. See the -{api-spring-framework}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] +{spring-framework-api}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] javadoc for details. diff --git a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc index a66ade956c27..afa9a50dc96a 100644 --- a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc +++ b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc @@ -56,7 +56,7 @@ alternate between read and write. == `PooledDataBuffer` As explained in the Javadoc for -https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html[ByteBuffer], +{java-api}/java.base/java/nio/ByteBuffer.html[ByteBuffer], byte buffers can be direct or non-direct. Direct buffers may reside outside the Java heap which eliminates the need for copying for native I/O operations. That makes direct buffers particularly useful for receiving and sending data over a socket, but they're also more diff --git a/framework-docs/modules/ROOT/pages/core/expressions.adoc b/framework-docs/modules/ROOT/pages/core/expressions.adoc index 6065d6ec9787..7700929f7b60 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions.adoc @@ -1,17 +1,18 @@ [[expressions]] = Spring Expression Language (SpEL) -The Spring Expression Language ("`SpEL`" for short) is a powerful expression language that +The Spring Expression Language ("SpEL" for short) is a powerful expression language that supports querying and manipulating an object graph at runtime. The language syntax is -similar to Unified EL but offers additional features, most notably method invocation and -basic string templating functionality. +similar to the https://jakarta.ee/specifications/expression-language/[Jakarta Expression +Language] but offers additional features, most notably method invocation and basic string +templating functionality. While there are several other Java expression languages available -- OGNL, MVEL, and JBoss EL, to name a few -- the Spring Expression Language was created to provide the Spring community with a single well supported expression language that can be used across all the products in the Spring portfolio. Its language features are driven by the requirements of the projects in the Spring portfolio, including tooling requirements -for code completion support within the https://spring.io/tools[Spring Tools for Eclipse]. +for code completion support within the {spring-site-tools}[Spring Tools for Eclipse]. That said, SpEL is based on a technology-agnostic API that lets other expression language implementations be integrated, should the need arise. @@ -33,24 +34,24 @@ populate them are listed at the end of the chapter. The expression language supports the following functionality: * Literal expressions -* Boolean and relational operators -* Regular expressions -* Class expressions * Accessing properties, arrays, lists, and maps -* Method invocation -* Assignment -* Calling constructors -* Bean references -* Array construction * Inline lists * Inline maps -* Ternary operator +* Array construction +* Relational operators +* Regular expressions +* Logical operators +* String operators +* Mathematical operators +* Assignment +* Type expressions +* Method invocation +* Constructor invocation * Variables * User-defined functions +* Bean references +* Ternary, Elvis, and safe-navigation operators * 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 2ed0079d67be..7a8ed5d421a4 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc @@ -1,12 +1,12 @@ [[expressions-evaluation]] = Evaluation -This section introduces the simple use of SpEL interfaces and its expression language. -The complete language reference can be found in +This section introduces programmatic use of SpEL's interfaces and its expression language. +The complete language reference can be found in the xref:core/expressions/language-ref.adoc[Language Reference]. -The following code introduces the SpEL API to evaluate the literal string expression, -`Hello World`. +The following code demonstrates how to use the SpEL API to evaluate the literal string +expression, `Hello World`. [tabs] ====== @@ -18,7 +18,7 @@ Java:: Expression exp = parser.parseExpression("'Hello World'"); // <1> String message = (String) exp.getValue(); ---- -<1> The value of the message variable is `'Hello World'`. +<1> The value of the message variable is `"Hello World"`. Kotlin:: + @@ -28,24 +28,24 @@ Kotlin:: val exp = parser.parseExpression("'Hello World'") // <1> val message = exp.value as String ---- -<1> The value of the message variable is `'Hello World'`. +<1> The value of the message variable is `"Hello World"`. ====== - The SpEL classes and interfaces you are most likely to use are located in the `org.springframework.expression` package and its sub-packages, such as `spel.support`. -The `ExpressionParser` interface is responsible for parsing an expression string. In -the preceding example, the expression string is a string literal denoted by the surrounding single -quotation marks. The `Expression` interface is responsible for evaluating the previously defined -expression string. Two exceptions that can be thrown, `ParseException` and -`EvaluationException`, when calling `parser.parseExpression` and `exp.getValue`, -respectively. +The `ExpressionParser` interface is responsible for parsing an expression string. In the +preceding example, the expression string is a string literal denoted by the surrounding +single quotation marks. The `Expression` interface is responsible for evaluating the +defined expression string. The two types of exceptions that can be thrown when calling +`parser.parseExpression(...)` and `exp.getValue(...)` are `ParseException` and +`EvaluationException`, respectively. -SpEL supports a wide range of features, such as calling methods, accessing properties, +SpEL supports a wide range of features such as calling methods, accessing properties, and calling constructors. -In the following example of method invocation, we call the `concat` method on the string literal: +In the following method invocation example, we call the `concat` method on the string +literal, `Hello World`. [tabs] ====== @@ -57,7 +57,7 @@ Java:: Expression exp = parser.parseExpression("'Hello World'.concat('!')"); // <1> String message = (String) exp.getValue(); ---- -<1> The value of `message` is now 'Hello World!'. +<1> The value of `message` is now `"Hello World!"`. Kotlin:: + @@ -67,10 +67,11 @@ Kotlin:: val exp = parser.parseExpression("'Hello World'.concat('!')") // <1> val message = exp.value as String ---- -<1> The value of `message` is now 'Hello World!'. +<1> The value of `message` is now `"Hello World!"`. ====== -The following example of calling a JavaBean property calls the `String` property `Bytes`: +The following example demonstrates how to access the `Bytes` JavaBean property of the +string literal, `Hello World`. [tabs] ====== @@ -100,10 +101,10 @@ Kotlin:: ====== SpEL also supports nested properties by using the standard dot notation (such as -`prop1.prop2.prop3`) and also the corresponding setting of property values. +`prop1.prop2.prop3`) as well as the corresponding setting of property values. Public fields may also be accessed. -The following example shows how to use dot notation to get the length of a literal: +The following example shows how to use dot notation to get the length of a string literal. [tabs] ====== @@ -133,7 +134,7 @@ Kotlin:: ====== The String's constructor can be called instead of using a string literal, as the following -example shows: +example shows. [tabs] ====== @@ -145,7 +146,7 @@ Java:: Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); // <1> String message = exp.getValue(String.class); ---- -<1> Construct a new `String` from the literal and make it be upper case. +<1> Construct a new `String` from the literal and convert it to upper case. Kotlin:: + @@ -155,10 +156,9 @@ Kotlin:: val exp = parser.parseExpression("new String('hello world').toUpperCase()") // <1> val message = exp.getValue(String::class.java) ---- -<1> Construct a new `String` from the literal and make it be upper case. +<1> Construct a new `String` from the literal and convert it to upper case. ====== - Note the use of the generic method: `public T getValue(Class desiredResultType)`. Using this method removes the need to cast the value of the expression to the desired result type. An `EvaluationException` is thrown if the value cannot be cast to the @@ -166,8 +166,8 @@ type `T` or converted by using the registered type converter. The more common usage of SpEL is to provide an expression string that is evaluated against a specific object instance (called the root object). The following example shows -how to retrieve the `name` property from an instance of the `Inventor` class or -create a boolean condition: +how to retrieve the `name` property from an instance of the `Inventor` class and how to +reference the `name` property in a boolean expression. [tabs] ====== @@ -222,29 +222,38 @@ Kotlin:: [[expressions-evaluation-context]] == Understanding `EvaluationContext` -The `EvaluationContext` interface is used when evaluating an expression to resolve -properties, methods, or fields and to help perform type conversion. Spring provides two +The `EvaluationContext` API is used when evaluating an expression to resolve properties, +methods, or fields and to help perform type conversion. Spring provides two implementations. -* `SimpleEvaluationContext`: Exposes a subset of essential SpEL language features and -configuration options, for categories of expressions that do not require the full extent -of the SpEL language syntax and should be meaningfully restricted. Examples include but -are not limited to data binding expressions and property-based filters. +`SimpleEvaluationContext`:: + Exposes a subset of essential SpEL language features and configuration options, for + categories of expressions that do not require the full extent of the SpEL language + syntax and should be meaningfully restricted. Examples include but are not limited to + data binding expressions and property-based filters. + +`StandardEvaluationContext`:: + Exposes the full set of SpEL language features and configuration options. You can use + it to specify a default root object and to configure every available evaluation-related + strategy. -* `StandardEvaluationContext`: Exposes the full set of SpEL language features and -configuration options. You can use it to specify a default root object and to configure -every available evaluation-related strategy. +`SimpleEvaluationContext` is designed to support only a subset of the SpEL language +syntax. For example, it excludes Java type references, constructors, and bean references. +It also requires you to explicitly choose the level of support for properties and methods +in expressions. When creating a `SimpleEvaluationContext` you need to choose the level of +support that you need for data binding in SpEL expressions: -`SimpleEvaluationContext` is designed to support only a subset of the SpEL language syntax. -It excludes Java type references, constructors, and bean references. It also requires -you to explicitly choose the level of support for properties and methods in expressions. -By default, the `create()` static factory method enables only read access to properties. -You can also obtain a builder to configure the exact level of support needed, targeting -one or some combination of the following: +* Data binding for read-only access +* Data binding for read and write access +* A custom `PropertyAccessor` (typically not reflection-based), potentially combined with + a `DataBindingPropertyAccessor` -* Custom `PropertyAccessor` only (no reflection) -* Data binding properties for read-only access -* Data binding properties for read and write +Conveniently, `SimpleEvaluationContext.forReadOnlyDataBinding()` enables read-only access +to properties via `DataBindingPropertyAccessor`. Similarly, +`SimpleEvaluationContext.forReadWriteDataBinding()` enables read and write access to +properties. Alternatively, configure custom accessors via +`SimpleEvaluationContext.forPropertyAccessors(...)`, potentially disable assignment, and +optionally activate method resolution and/or a type converter through the builder. [[expressions-type-conversion]] @@ -252,16 +261,15 @@ one or some combination of the following: By default, SpEL uses the conversion service available in Spring core (`org.springframework.core.convert.ConversionService`). This conversion service comes -with many built-in converters for common conversions but is also fully extensible so that -you can add custom conversions between types. Additionally, it is -generics-aware. This means that, when you work with generic types in -expressions, SpEL attempts conversions to maintain type correctness for any objects -it encounters. +with many built-in converters for common conversions, but is also fully extensible so +that you can add custom conversions between types. Additionally, it is generics-aware. +This means that, when you work with generic types in expressions, SpEL attempts +conversions to maintain type correctness for any objects it encounters. What does this mean in practice? Suppose assignment, using `setValue()`, is being used to set a `List` property. The type of the property is actually `List`. SpEL recognizes that the elements of the list need to be converted to `Boolean` before -being placed in it. The following example shows how to do so: +being placed in it. The following example shows how to do so. [tabs] ====== @@ -315,17 +323,17 @@ 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] ====== @@ -380,16 +388,25 @@ Kotlin:: ---- ====== +By default, a SpEL expression cannot contain more than 10,000 characters; however, the +`maxExpressionLength` is configurable. If you create a `SpelExpressionParser` +programmatically, you can specify a custom `maxExpressionLength` when creating the +`SpelParserConfiguration` that you provide to the `SpelExpressionParser`. If you wish to +set the `maxExpressionLength` used for parsing SpEL expressions within an +`ApplicationContext` -- for example, in XML bean definitions, `@Value`, etc. -- you can +set a JVM system property or Spring property named `spring.context.expression.maxLength` +to the maximum expression length needed by your application (see +xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]). [[expressions-spel-compilation]] == SpEL Compilation -Spring Framework 4.1 includes a basic expression compiler. Expressions are usually -interpreted, which provides a lot of dynamic flexibility during evaluation but -does not provide optimum performance. For occasional expression usage, -this is fine, but, when used by other components such as Spring Integration, -performance can be very important, and there is no real need for the dynamism. +Spring provides a basic compiler for SpEL expressions. Expressions are usually +interpreted, which provides a lot of dynamic flexibility during evaluation but does not +provide optimum performance. For occasional expression usage, this is fine, but, when +used by other components such as Spring Integration, performance can be very important, +and there is no real need for the dynamism. The SpEL compiler is intended to address this need. During evaluation, the compiler generates a Java class that embodies the expression behavior at runtime and uses that @@ -402,16 +419,17 @@ information can cause trouble later if the types of the various expression eleme change over time. For this reason, compilation is best suited to expressions whose type information is not going to change on repeated evaluations. -Consider the following basic expression: +Consider the following basic expression. +[source,java,indent=0,subs="verbatim,quotes"] ---- -someArray[0].someProperty.someOtherProperty < 0.1 + someArray[0].someProperty.someOtherProperty < 0.1 ---- -Because the preceding expression involves array access, some property de-referencing, -and numeric operations, the performance gain can be very noticeable. In an example -micro benchmark run of 50000 iterations, it took 75ms to evaluate by using the -interpreter and only 3ms using the compiled version of the expression. +Because the preceding expression involves array access, some property de-referencing, and +numeric operations, the performance gain can be very noticeable. In an example micro +benchmark run of 50,000 iterations, it took 75ms to evaluate by using the interpreter and +only 3ms using the compiled version of the expression. [[expressions-compiler-configuration]] @@ -419,33 +437,34 @@ interpreter and only 3ms using the compiled version of the expression. The compiler is not turned on by default, but you can turn it on in either of two different ways. You can turn it on by using the parser configuration process -(xref:core/expressions/evaluation.adoc#expressions-parser-configuration[discussed earlier]) or by using a Spring property -when SpEL usage is embedded inside another component. This section discusses both of -these options. +(xref:core/expressions/evaluation.adoc#expressions-parser-configuration[discussed +earlier]) or by using a Spring property when SpEL usage is embedded inside another +component. This section discusses both of these options. The compiler can operate in one of three modes, which are captured in the -`org.springframework.expression.spel.SpelCompilerMode` enum. The modes are as follows: +`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. + 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. `IMMEDIATE` mode exists because `MIXED` mode could cause issues for expressions that have side effects. If a compiled expression blows up after partially succeeding, it may have already done something that has affected the state of the system. If this has happened, the caller may not want it to silently re-run in interpreted mode, -since part of the expression may be running twice. +since part of the expression may be run twice. After selecting a mode, use the `SpelParserConfiguration` to configure the parser. The -following example shows how to do so: +following example shows how to do so. [tabs] ====== @@ -482,15 +501,16 @@ Kotlin:: ---- ====== -When you specify the compiler mode, you can also specify a classloader (passing null is allowed). -Compiled expressions are defined in a child classloader created under any that is supplied. -It is important to ensure that, if a classloader is specified, it can see all the types involved in -the expression evaluation process. If you do not specify a classloader, a default classloader is used -(typically the context classloader for the thread that is running during expression evaluation). +When you specify the compiler mode, you can also specify a `ClassLoader` (passing `null` +is allowed). Compiled expressions are defined in a child `ClassLoader` created under any +that is supplied. It is important to ensure that, if a `ClassLoader` is specified, it can +see all the types involved in the expression evaluation process. If you do not specify a +`ClassLoader`, a default `ClassLoader` is used (typically the context `ClassLoader` for +the thread that is running during expression evaluation). The second way to configure the compiler is for use when SpEL is embedded inside some other component and it may not be possible to configure it through a configuration -object. In these cases, it is possible to set the `spring.expression.compiler.mode` +object. In such cases, it is possible to set the `spring.expression.compiler.mode` property via a JVM system property (or via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) to one of the `SpelCompilerMode` enum values (`off`, `immediate`, or `mixed`). @@ -499,18 +519,16 @@ xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) to [[expressions-compiler-limitations]] === Compiler Limitations -Since Spring Framework 4.1, the basic compilation framework is in place. However, the framework -does not yet support compiling every kind of expression. The initial focus has been on the -common expressions that are likely to be used in performance-critical contexts. The following -kinds of expression cannot be compiled at the moment: +Spring does not support compiling every kind of expression. The primary focus is on +common expressions that are likely to be used in performance-critical contexts. The +following kinds of expressions cannot be compiled. * Expressions involving assignment * Expressions relying on the conversion service * Expressions using custom resolvers or accessors +* Expressions using overloaded operators +* Expressions using array construction syntax * Expressions using selection or projection -More types of expressions will be compilable in the future. - - - +Compilation of additional kinds of expressions may be supported in the future. diff --git a/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc b/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc index e94e3b8c091c..b85702e3ae7b 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc @@ -3,9 +3,11 @@ This section lists the classes used in the examples throughout this chapter. +== `Inventor` + [tabs] ====== -Inventor.Java:: +Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] ---- @@ -80,7 +82,7 @@ Inventor.Java:: } ---- -Inventor.kt:: +Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] ---- @@ -95,9 +97,11 @@ Inventor.kt:: ---- ====== +== `PlaceOfBirth` + [tabs] ====== -PlaceOfBirth.java:: +Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] ---- @@ -135,7 +139,7 @@ PlaceOfBirth.java:: } ---- -PlaceOfBirth.kt:: +Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] ---- @@ -145,9 +149,11 @@ PlaceOfBirth.kt:: ---- ====== +== `Society` + [tabs] ====== -Society.java:: +Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] ---- @@ -192,7 +198,7 @@ Society.java:: } ---- -Society.kt:: +Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] ---- 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 f920ac60470e..ebce4c294cc4 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc @@ -2,24 +2,4 @@ = Language Reference :page-section-summary-toc: 1 -This section describes how the Spring Expression Language works. It covers the following -topics: - -* xref:core/expressions/language-ref/literal.adoc[Literal Expressions] -* xref:core/expressions/language-ref/properties-arrays.adoc[Properties, Arrays, Lists, Maps, and Indexers] -* xref:core/expressions/language-ref/inline-lists.adoc[Inline Lists] -* xref:core/expressions/language-ref/inline-maps.adoc[Inline Maps] -* xref:core/expressions/language-ref/array-construction.adoc[Array Construction] -* xref:core/expressions/language-ref/methods.adoc[Methods] -* xref:core/expressions/language-ref/operators.adoc[Operators] -* xref:core/expressions/language-ref/types.adoc[Types] -* xref:core/expressions/language-ref/constructors.adoc[Constructors] -* xref:core/expressions/language-ref/variables.adoc[Variables] -* xref:core/expressions/language-ref/functions.adoc[Functions] -* xref:core/expressions/language-ref/bean-references.adoc[Bean References] -* xref:core/expressions/language-ref/operator-ternary.adoc[Ternary Operator (If-Then-Else)] -* xref:core/expressions/language-ref/operator-elvis.adoc[The Elvis Operator] -* xref:core/expressions/language-ref/operator-safe-navigation.adoc[Safe Navigation Operator] - - - +This section describes how the Spring Expression Language works. diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/array-construction.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/array-construction.adoc index 01e181ae3017..4bc931742295 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 @@ -13,7 +13,7 @@ Java:: int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); // Array with initializer - int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context); + int[] numbers2 = (int[]) parser.parseExpression("new int[] {1, 2, 3}").getValue(context); // Multi dimensional array int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context); @@ -26,14 +26,22 @@ Kotlin:: val numbers1 = parser.parseExpression("new int[4]").getValue(context) as IntArray // Array with initializer - val numbers2 = parser.parseExpression("new int[]{1,2,3}").getValue(context) as IntArray + val numbers2 = parser.parseExpression("new int[] {1, 2, 3}").getValue(context) as IntArray // Multi dimensional array val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- ====== +[NOTE] +==== You cannot currently supply an initializer when you construct a multi-dimensional array. - - - +==== + +[CAUTION] +==== +Any expression that constructs an array – for example, via `new int[4]` or +`new int[] {1, 2, 3}` – cannot be compiled. See +xref:core/expressions/evaluation.adoc#expressions-compiler-limitations[Compiler Limitations] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc index 83f492766dd0..78a3ec21fe4f 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc @@ -4,7 +4,7 @@ Projection lets a collection drive the evaluation of a sub-expression, and the result is a new collection. The syntax for projection is `.![projectionExpression]`. For example, suppose we have a list of inventors but want the list of cities where they were born. -Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +Effectively, we want to evaluate `placeOfBirth.city` for every entry in the inventor list. The following example uses projection to do so: [tabs] @@ -13,16 +13,18 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - // returns ['Smiljan', 'Idvor' ] - List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]"); + // evaluates to ["Smiljan", "Idvor"] + List placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") + .getValue(societyContext, List.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // returns ['Smiljan', 'Idvor' ] - val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> + // evaluates to ["Smiljan", "Idvor"] + val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") + .getValue(societyContext) as List<*> ---- ====== @@ -32,5 +34,12 @@ evaluated against each entry in the map (represented as a Java `Map.Entry`). The of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. +[NOTE] +==== +The Spring Expression Language also supports safe navigation for collection projection. +See +xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc index 3f87541a81cb..b87bc1733413 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 @@ -28,13 +28,14 @@ Kotlin:: ====== Selection is supported for arrays and anything that implements `java.lang.Iterable` or -`java.util.Map`. For a list or array, the selection criteria is evaluated against each -individual element. Against a map, the selection criteria is evaluated against each map -entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` -accessible as properties for use in the selection. +`java.util.Map`. For an array or `Iterable`, the selection expression is evaluated +against each individual element. Against a map, the selection expression is evaluated +against each map entry (objects of the Java type `Map.Entry`). Each map entry has its +`key` and `value` accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the -original map where the entry's value is less than 27: +Given a `Map` stored in a variable named `#map`, the following expression returns a new +map that consists of those elements of the original map where the entry's value is less +than 27: [tabs] ====== @@ -42,21 +43,28 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - Map newMap = parser.parseExpression("map.?[value<27]").getValue(); + Map newMap = parser.parseExpression("#map.?[value < 27]").getValue(Map.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - val newMap = parser.parseExpression("map.?[value<27]").getValue() + val newMap = parser.parseExpression("#map.?[value < 27]").getValue() as Map ---- ====== In addition to returning all the selected elements, you can retrieve only the first or -the last element. To obtain the first element matching the selection, the syntax is -`.^[selectionExpression]`. To obtain the last matching selection, the syntax is -`.$[selectionExpression]`. +the last element. To obtain the first element matching the selection expression, the +syntax is `.^[selectionExpression]`. To obtain the last element matching the selection +expression, the syntax is `.$[selectionExpression]`. +[NOTE] +==== +The Spring Expression Language also supports safe navigation for collection selection. +See +xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc index 014d6e8b8c44..6e1e2bf81f64 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc @@ -1,9 +1,26 @@ [[expressions-ref-functions]] = Functions -You can extend SpEL by registering user-defined functions that can be called within the -expression string. The function is registered through the `EvaluationContext`. The -following example shows how to register a user-defined function: +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. + +[TIP] +==== +`StandardEvaluationContext` also defines `registerFunction(...)` methods that provide a +convenient way to register a function as a `java.lang.reflect.Method` or a +`java.lang.invoke.MethodHandle`. +==== + +[WARNING] +==== +Since functions share a common namespace with +xref:core/expressions/language-ref/variables.adoc[variables] in the evaluation context, +care must be taken to ensure that function names and variable names do not overlap. +==== + +The following example shows how to register a user-defined function to be invoked via +reflection using a `java.lang.reflect.Method`: [tabs] ====== @@ -39,11 +56,7 @@ Java:: public abstract class StringUtils { public static String reverseString(String input) { - StringBuilder backwards = new StringBuilder(input.length()); - for (int i = 0; i < input.length(); i++) { - backwards.append(input.charAt(input.length() - 1 - i)); - } - return backwards.toString(); + return new StringBuilder(input).reverse().toString(); } } ---- @@ -53,16 +66,12 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- fun reverseString(input: String): String { - val backwards = StringBuilder(input.length) - for (i in 0 until input.length) { - backwards.append(input[input.length - 1 - i]) - } - return backwards.toString() + return StringBuilder(input).reverse().toString() } ---- ====== -You can then register and use the preceding method, as the following example shows: +You can register and use the preceding method, as the following example shows: [tabs] ====== @@ -74,8 +83,9 @@ Java:: EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable("reverseString", - StringUtils.class.getDeclaredMethod("reverseString", String.class)); + StringUtils.class.getMethod("reverseString", String.class)); + // evaluates to "olleh" String helloWorldReversed = parser.parseExpression( "#reverseString('hello')").getValue(context, String.class); ---- @@ -87,12 +97,108 @@ Kotlin:: val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() - context.setVariable("reverseString", ::reverseString::javaMethod) + context.setVariable("reverseString", ::reverseString.javaMethod) + // evaluates to "olleh" val helloWorldReversed = parser.parseExpression( "#reverseString('hello')").getValue(context, String::class.java) ---- ====== +A function can also be registered as a `java.lang.invoke.MethodHandle`. This enables +potentially more efficient use cases if the `MethodHandle` target and parameters have +been fully bound prior to registration; however, partially bound handles are also +supported. + +Consider the `String#formatted(String, Object...)` instance method, which produces a +message according to a template and a variable number of arguments. + +You can register and use the `formatted` method as a `MethodHandle`, as the following +example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + + MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted", + MethodType.methodType(String.class, Object[].class)); + context.setVariable("message", mh); + + // evaluates to "Simple message: " + String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')") + .getValue(context, String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() + + val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted", + MethodType.methodType(String::class.java, Array::class.java)) + context.setVariable("message", mh) + + // evaluates to "Simple message: " + val message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')") + .getValue(context, String::class.java) +---- +====== + +As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also +supported. This is likely to be more performant if both the target and all the arguments +are bound. In that case no arguments are necessary in the SpEL expression, as the +following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + + 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)) + .bindTo(template) + .bindTo(varargs); //here we have to provide arguments in a single array binding + context.setVariable("message", mh); + + // evaluates to "This is a prerecorded message with 3 words: " + String message = parser.parseExpression("#message()") + .getValue(context, String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() + + val template = "This is a %s message with %s words: <%s>" + 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)) + .bindTo(template) + .bindTo(varargs) //here we have to provide arguments in a single array binding + context.setVariable("message", mh) + + // evaluates to "This is a prerecorded message with 3 words: " + val message = parser.parseExpression("#message()") + .getValue(context, String::class.java) +---- +====== + diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc index c133af0f367a..52b1c9c06ced 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc @@ -3,20 +3,46 @@ SpEL supports the following types of literal expressions. -- strings -- numeric values: integer (`int` or `long`), hexadecimal (`int` or `long`), real (`float` - or `double`) -- boolean values: `true` or `false` -- null - -Strings can delimited by single quotation marks (`'`) or double quotation marks (`"`). To -include a single quotation mark within a string literal enclosed in single quotation -marks, use two adjacent single quotation mark characters. Similarly, to include a double -quotation mark within a string literal enclosed in double quotation marks, use two -adjacent double quotation mark characters. - -Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using `Double.parseDouble()`. +String :: + Strings can be delimited by single quotation marks (`'`) or double quotation marks + (`"`). To include a single quotation mark within a string literal enclosed in single + quotation marks, use two adjacent single quotation mark characters. Similarly, to + include a double quotation mark within a string literal enclosed in double quotation + marks, use two adjacent double quotation mark characters. +Number :: + Numbers support the use of the negative sign, exponential notation, and decimal points. + * Integer: `int` or `long` + * Hexadecimal: `int` or `long` + * Real: `float` or `double` + ** By default, real numbers are parsed using `Double.parseDouble()`. +Boolean :: + `true` or `false` +Null :: + `null` + +[NOTE] +==== +Due to the design and implementation of the Spring Expression Language, literal numbers +are always stored internally as positive numbers. + +For example, `-2` is stored internally as a positive `2` which is then negated while +evaluating the expression (by calculating the value of `0 - 2`). + +This means that it is not possible to represent a negative literal number equal to the +minimum value of that type of number in Java. For example, the minimum supported value +for an `int` in Java is `Integer.MIN_VALUE` which has a value of `-2147483648`. However, +if you include `-2147483648` in a SpEL expression, an exception will be thrown informing +you that the value `2147483648` cannot be parsed as an `int` (because it exceeds the +value of `Integer.MAX_VALUE` which is `2147483647`). + +If you need to use the minimum value for a particular type of number within a SpEL +expression, you can either reference the `MIN_VALUE` constant for the respective wrapper +type (such as `Integer.MIN_VALUE`, `Long.MIN_VALUE`, etc.) or calculate the minimum +value. For example, to use the minimum integer value: + +- `T(Integer).MIN_VALUE` -- requires a `StandardEvaluationContext` +- `-2^31` -- can be used with any type of `EvaluationContext` +==== The following listing shows simple usage of literals. Typically, they are not used in isolation like this but, rather, as part of a more complex expression -- for example, 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 8f7e69a0b6cb..3884dcadf2f7 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 @@ -38,6 +38,10 @@ Kotlin:: ---- ====== +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). + The following listing shows a more complex example: [tabs] @@ -53,7 +57,7 @@ Java:: String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); System.out.println(name); // Nikola Tesla - tesla.setName(null); + tesla.setName(""); name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); System.out.println(name); // Elvis Presley ---- @@ -69,7 +73,7 @@ Kotlin:: var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) println(name) // Nikola Tesla - tesla.setName(null) + tesla.setName("") name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) println(name) // Elvis Presley ---- diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc index 0691cf2de87a..d914a7002965 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc @@ -1,12 +1,27 @@ [[expressions-operator-safe-navigation]] = Safe Navigation Operator -The safe navigation operator is used to avoid a `NullPointerException` and comes from -the https://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] -language. Typically, when you have a reference to an object, you might need to verify that -it is not null before accessing methods or properties of the object. To avoid this, the -safe navigation operator returns null instead of throwing an exception. The following -example shows how to use the safe navigation operator: +The safe navigation operator (`?`) is used to avoid a `NullPointerException` and comes +from the https://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] +language. Typically, when you have a reference to an object, you might need to verify +that it is not `null` before accessing methods or properties of the object. To avoid +this, the safe navigation operator returns `null` for the particular null-safe operation +instead of throwing an exception. + +[WARNING] +==== +When the safe navigation operator evaluates to `null` for a particular null-safe +operation within a compound expression, the remainder of the compound expression will +still be evaluated. + +See <> for details. +==== + +[[expressions-operator-safe-navigation-property-access]] +== Safe Property and Method Access + +The following example shows how to use the safe navigation operator for property access +(`?.`). [tabs] ====== @@ -20,13 +35,18 @@ Java:: Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan")); - String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); - System.out.println(city); // Smiljan + // evaluates to "Smiljan" + String city = parser.parseExpression("placeOfBirth?.city") // <1> + .getValue(context, tesla, String.class); tesla.setPlaceOfBirth(null); - city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); - System.out.println(city); // null - does not throw NullPointerException!!! + + // evaluates to null - does not throw NullPointerException + city = parser.parseExpression("placeOfBirth?.city") // <2> + .getValue(context, tesla, String.class); ---- +<1> Use safe navigation operator on non-null `placeOfBirth` property +<2> Use safe navigation operator on null `placeOfBirth` property Kotlin:: + @@ -38,14 +58,306 @@ Kotlin:: val tesla = Inventor("Nikola Tesla", "Serbian") tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan")) - var city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) - println(city) // Smiljan + // evaluates to "Smiljan" + var city = parser.parseExpression("placeOfBirth?.city") // <1> + .getValue(context, tesla, String::class.java) tesla.setPlaceOfBirth(null) - city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) - println(city) // null - does not throw NullPointerException!!! + + // evaluates to null - does not throw NullPointerException + city = parser.parseExpression("placeOfBirth?.city") // <2> + .getValue(context, tesla, String::class.java) +---- +<1> Use safe navigation operator on non-null `placeOfBirth` property +<2> Use safe navigation operator on null `placeOfBirth` property +====== + +[NOTE] +==== +The safe navigation operator also applies to method invocations on an object. + +For example, the expression `#calculator?.max(4, 2)` evaluates to `null` if the +`#calculator` variable has not been configured in the context. Otherwise, the +`max(int, int)` method will be invoked on the `#calculator`. +==== + + +[[expressions-operator-safe-navigation-selection-and-projection]] +== Safe Collection Selection and Projection + +The Spring Expression Language supports safe navigation for +xref:core/expressions/language-ref/collection-selection.adoc[collection selection] and +xref:core/expressions/language-ref/collection-projection.adoc[collection projection] via +the following operators. + +* null-safe selection: `?.?` +* null-safe select first: `?.^` +* null-safe select last: `?.$` +* null-safe projection: `?.!` + +The following example shows how to use the safe navigation operator for collection +selection (`?.?`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = "members?.?[nationality == 'Serbian']"; // <1> + + // evaluates to [Inventor("Nikola Tesla")] + List list = (List) parser.parseExpression(expression) + .getValue(context); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + list = (List) parser.parseExpression(expression) + .getValue(context); +---- +<1> Use null-safe selection operator on potentially null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = "members?.?[nationality == 'Serbian']" // <1> + + // evaluates to [Inventor("Nikola Tesla")] + var list = parser.parseExpression(expression) + .getValue(context) as List + + society.members = null + + // evaluates to null - does not throw a NullPointerException + list = parser.parseExpression(expression) + .getValue(context) as List +---- +<1> Use null-safe selection operator on potentially null `members` list +====== + +The following example shows how to use the "null-safe select first" operator for +collections (`?.^`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = + "members?.^[nationality == 'Serbian' || nationality == 'Idvor']"; // <1> + + // evaluates to Inventor("Nikola Tesla") + Inventor inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); +---- +<1> Use "null-safe select first" operator on potentially null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = + "members?.^[nationality == 'Serbian' || nationality == 'Idvor']" // <1> + + // evaluates to Inventor("Nikola Tesla") + var inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) +---- +<1> Use "null-safe select first" operator on potentially null `members` list +====== + + +The following example shows how to use the "null-safe select last" operator for +collections (`?.$`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = + "members?.$[nationality == 'Serbian' || nationality == 'Idvor']"; // <1> + + // evaluates to Inventor("Pupin") + Inventor inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); +---- +<1> Use "null-safe select last" operator on potentially null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = + "members?.$[nationality == 'Serbian' || nationality == 'Idvor']" // <1> + + // evaluates to Inventor("Pupin") + var inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) ---- +<1> Use "null-safe select last" operator on potentially null `members` list ====== +The following example shows how to use the safe navigation operator for collection +projection (`?.!`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + // evaluates to ["Smiljan", "Idvor"] + List placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1> + .getValue(context, List.class); + society.members = null; + + // evaluates to null - does not throw a NullPointerException + placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2> + .getValue(context, List.class); +---- +<1> Use null-safe projection operator on non-null `members` list +<2> Use null-safe projection operator on null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + + // evaluates to ["Smiljan", "Idvor"] + var placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1> + .getValue(context, List::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2> + .getValue(context, List::class.java) +---- +<1> Use null-safe projection operator on non-null `members` list +<2> Use null-safe projection operator on null `members` list +====== + + +[[expressions-operator-safe-navigation-compound-expressions]] +== Null-safe Operations in Compound Expressions + +As mentioned at the beginning of this section, when the safe navigation operator +evaluates to `null` for a particular null-safe operation within a compound expression, +the remainder of the compound expression will still be evaluated. This means that the +safe navigation operator must be applied throughout a compound expression in order to +avoid any unwanted `NullPointerException`. + +Given the expression `#person?.address.city`, if `#person` is `null` the safe navigation +operator (`?.`) ensures that no exception will be thrown when attempting to access the +`address` property of `#person`. However, since `#person?.address` evaluates to `null`, a +`NullPointerException` will be thrown when attempting to access the `city` property of +`null`. To address that, you can apply null-safe navigation throughout the compound +expression as in `#person?.address?.city`. That expression will safely evaluate to `null` +if either `#person` or `#person?.address` evaluates to `null`. + +The following example demonstrates how to use the "null-safe select first" operator +(`?.^`) on a collection combined with null-safe property access (`?.`) within a compound +expression. If `members` is `null`, the result of the "null-safe select first" operator +(`members?.^[nationality == 'Serbian']`) evaluates to `null`, and the additional use of +the safe navigation operator (`?.name`) ensures that the entire compound expression +evaluates to `null` instead of throwing an exception. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = "members?.^[nationality == 'Serbian']?.name"; // <1> + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression(expression) + .getValue(context, String.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + name = parser.parseExpression(expression) + .getValue(context, String.class); +---- +<1> Use "null-safe select first" and null-safe property access operators within compound expression. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = "members?.^[nationality == 'Serbian']?.name" // <1> + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression(expression) + .getValue(context, String::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + name = parser.parseExpression(expression) + .getValue(context, String::class.java) +---- +<1> Use "null-safe select first" and null-safe property access operators within compound expression. +====== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc index 7d9d6ece8e4e..f658d77d4410 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc @@ -5,8 +5,11 @@ The Spring Expression Language supports the following kinds of operators: * xref:core/expressions/language-ref/operators.adoc#expressions-operators-relational[Relational Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-operators-logical[Logical Operators] +* xref:core/expressions/language-ref/operators.adoc#expressions-operators-string[String Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-operators-mathematical[Mathematical Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-assignment[The Assignment Operator] +* xref:core/expressions/language-ref/operators.adoc#expressions-operators-overloaded[Overloaded Operators] + [[expressions-operators-relational]] @@ -15,7 +18,7 @@ The Spring Expression Language supports the following kinds of operators: The relational operators (equal, not equal, less than, less than or equal, greater than, and greater than or equal) are supported by using standard operator notation. These operators work on `Number` types as well as types implementing `Comparable`. -The following listing shows a few examples of operators: +The following listing shows a few examples of relational operators: [tabs] ====== @@ -65,8 +68,22 @@ If you prefer numeric comparisons instead, avoid number-based `null` comparisons in favor of comparisons against zero (for example, `X > 0` or `X < 0`). ==== -In addition to the standard relational operators, SpEL supports the `instanceof` and regular -expression-based `matches` operator. The following listing shows examples of both: +Each symbolic operator can also be specified as a purely textual equivalent. This avoids +problems where the symbols used have special meaning for the document type in which the +expression is embedded (such as in an XML document). The textual equivalents are: + +* `lt` (`<`) +* `gt` (`>`) +* `le` (`\<=`) +* `ge` (`>=`) +* `eq` (`==`) +* `ne` (`!=`) + +All of the textual operators are case-insensitive. + +In addition to the standard relational operators, SpEL supports the `between`, +`instanceof`, and regular expression-based `matches` operators. The following listing +shows examples of all three: [tabs] ====== @@ -74,16 +91,38 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- + boolean result; + + // evaluates to true + result = parser.parseExpression( + "1 between {1, 5}").getValue(Boolean.class); + + // evaluates to false + result = parser.parseExpression( + "1 between {10, 15}").getValue(Boolean.class); + + // evaluates to true + result = parser.parseExpression( + "'elephant' between {'aardvark', 'zebra'}").getValue(Boolean.class); + // evaluates to false - boolean falseValue = parser.parseExpression( + result = parser.parseExpression( + "'elephant' between {'aardvark', 'cobra'}").getValue(Boolean.class); + + // evaluates to true + result = parser.parseExpression( + "123 instanceof T(Integer)").getValue(Boolean.class); + + // evaluates to false + result = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean.class); // evaluates to true - boolean trueValue = parser.parseExpression( + result = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); // evaluates to false - boolean falseValue = parser.parseExpression( + result = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -91,50 +130,65 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- + // evaluates to true + var result = parser.parseExpression( + "1 between {1, 5}").getValue(Boolean::class.java) + + // evaluates to false + result = parser.parseExpression( + "1 between {10, 15}").getValue(Boolean::class.java) + + // evaluates to true + result = parser.parseExpression( + "'elephant' between {'aardvark', 'zebra'}").getValue(Boolean::class.java) + + // evaluates to false + result = parser.parseExpression( + "'elephant' between {'aardvark', 'cobra'}").getValue(Boolean::class.java) + + // evaluates to true + result = parser.parseExpression( + "123 instanceof T(Integer)").getValue(Boolean::class.java) + // evaluates to false - val falseValue = parser.parseExpression( + result = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean::class.java) // evaluates to true - val trueValue = parser.parseExpression( + result = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) // evaluates to false - val falseValue = parser.parseExpression( + result = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- ====== -CAUTION: Be careful with primitive types, as they are immediately boxed up to their -wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while -`1 instanceof T(Integer)` evaluates to `true`, as expected. - -Each symbolic operator can also be specified as a purely alphabetic equivalent. This -avoids problems where the symbols used have special meaning for the document type in -which the expression is embedded (such as in an XML document). The textual equivalents are: +[CAUTION] +==== +The syntax for the `between` operator is ` between {, }`, +which is effectively a shortcut for ` >= && \<= }`. -* `lt` (`<`) -* `gt` (`>`) -* `le` (`\<=`) -* `ge` (`>=`) -* `eq` (`==`) -* `ne` (`!=`) -* `div` (`/`) -* `mod` (`%`) -* `not` (`!`). +Consequently, `1 between {1, 5}` evaluates to `true`, while `1 between {5, 1}` evaluates +to `false`. +==== -All of the textual operators are case-insensitive. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`. [[expressions-operators-logical]] == Logical Operators -SpEL supports the following logical operators: +SpEL supports the following logical (`boolean`) operators: * `and` (`&&`) * `or` (`||`) * `not` (`!`) +All of the textual operators are case-insensitive. + The following example shows how to use the logical operators: [tabs] @@ -167,6 +221,7 @@ Java:: boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class); // -- AND and NOT -- + String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"; boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); ---- @@ -199,20 +254,105 @@ Kotlin:: val falseValue = parser.parseExpression("!true").getValue(Boolean::class.java) // -- AND and NOT -- + val expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')" val falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) ---- ====== +[[expressions-operators-string]] +== String Operators + +You can use the following operators on strings. + +* concatenation (`+`) +* subtraction (`-`) + - for use with a string containing a single character +* repeat (`*`) + +The following example shows the `String` operators in use: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + // -- Concatenation -- + + // evaluates to "hello world" + String helloWorld = parser.parseExpression("'hello' + ' ' + 'world'") + .getValue(String.class); + + // -- Character Subtraction -- + + // evaluates to 'a' + char ch = parser.parseExpression("'d' - 3") + .getValue(char.class); + + // -- Repeat -- + + // evaluates to "abcabc" + String repeated = parser.parseExpression("'abc' * 2") + .getValue(String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + // -- Concatenation -- + + // evaluates to "hello world" + val helloWorld = parser.parseExpression("'hello' + ' ' + 'world'") + .getValue(String::class.java) + + // -- Character Subtraction -- + + // evaluates to 'a' + val ch = parser.parseExpression("'d' - 3") + .getValue(Character::class.java); + + // -- Repeat -- + + // evaluates to "abcabc" + val repeated = parser.parseExpression("'abc' * 2") + .getValue(String::class.java); +---- +====== + [[expressions-operators-mathematical]] == Mathematical Operators -You can use the addition operator (`+`) on both numbers and strings. You can use the -subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. -You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. -Standard operator precedence is enforced. The following example shows the mathematical -operators in use: +You can use the following operators on numbers, and standard operator precedence is enforced. + +* addition (`+`) +* subtraction (`-`) +* increment (`{pp}`) +* decrement (`--`) +* multiplication (`*`) +* division (`/`) +* modulus (`%`) +* exponential power (`^`) + +The division and modulus operators can also be specified as a purely textual equivalent. +This avoids problems where the symbols used have special meaning for the document type in +which the expression is embedded (such as in an XML document). The textual equivalents +are: + +* `div` (`/`) +* `mod` (`%`) + +All of the textual operators are case-insensitive. + +[NOTE] +==== +The increment and decrement operators can be used with either prefix (`{pp}A`, `--A`) or +postfix (`A{pp}`, `A--`) notation with variables or properties that can be written to. +==== + +The following example shows the mathematical operators in use: [tabs] ====== @@ -220,67 +360,131 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - // Addition - int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2 + Inventor inventor = new Inventor(); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + + // -- Addition -- + + int two = parser.parseExpression("1 + 1").getValue(int.class); // 2 + + // -- Subtraction -- + + int four = parser.parseExpression("1 - -3").getValue(int.class); // 4 + + double d = parser.parseExpression("1000.00 - 1e4").getValue(double.class); // -9000 + + // -- Increment -- + + // The counter property in Inventor has an initial value of 0. - String testString = parser.parseExpression( - "'test' + ' ' + 'string'").getValue(String.class); // 'test string' + // evaluates to 2; counter is now 1 + two = parser.parseExpression("counter++ + 2").getValue(context, inventor, int.class); - // Subtraction - int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4 + // evaluates to 5; counter is now 2 + int five = parser.parseExpression("3 + ++counter").getValue(context, inventor, int.class); - double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000 + // -- Decrement -- - // Multiplication - int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6 + // The counter property in Inventor has a value of 2. - double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0 + // evaluates to 6; counter is now 1 + int six = parser.parseExpression("counter-- + 4").getValue(context, inventor, int.class); - // Division - int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2 + // evaluates to 5; counter is now 0 + five = parser.parseExpression("5 + --counter").getValue(context, inventor, int.class); - double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0 + // -- Multiplication -- - // Modulus - int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3 + six = parser.parseExpression("-2 * -3").getValue(int.class); // 6 - int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1 + double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(double.class); // 24.0 - // Operator precedence - int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21 + // -- Division -- + + int minusTwo = parser.parseExpression("6 / -3").getValue(int.class); // -2 + + double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(double.class); // 1.0 + + // -- Modulus -- + + int three = parser.parseExpression("7 % 4").getValue(int.class); // 3 + + int oneInt = parser.parseExpression("8 / 5 % 2").getValue(int.class); // 1 + + // -- Exponential power -- + + int maxInt = parser.parseExpression("(2^31) - 1").getValue(int.class); // Integer.MAX_VALUE + + int minInt = parser.parseExpression("-2^31").getValue(int.class); // Integer.MIN_VALUE + + // -- Operator precedence -- + + int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(int.class); // -21 ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Addition - val two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2 + val inventor = Inventor() + val context = SimpleEvaluationContext.forReadWriteDataBinding().build() + + // -- Addition -- - val testString = parser.parseExpression( - "'test' + ' ' + 'string'").getValue(String::class.java) // 'test string' + var two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2 + + // -- Subtraction -- - // Subtraction val four = parser.parseExpression("1 - -3").getValue(Int::class.java) // 4 val d = parser.parseExpression("1000.00 - 1e4").getValue(Double::class.java) // -9000 - // Multiplication - val six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6 + // -- Increment -- + + // The counter property in Inventor has an initial value of 0. + + // evaluates to 2; counter is now 1 + two = parser.parseExpression("counter++ + 2").getValue(context, inventor, Int::class.java) + + // evaluates to 5; counter is now 2 + var five = parser.parseExpression("3 + ++counter").getValue(context, inventor, Int::class.java) + + // -- Decrement -- + + // The counter property in Inventor has a value of 2. + + // evaluates to 6; counter is now 1 + var six = parser.parseExpression("counter-- + 4").getValue(context, inventor, Int::class.java) + + // evaluates to 5; counter is now 0 + five = parser.parseExpression("5 + --counter").getValue(context, inventor, Int::class.java) + + // -- Multiplication -- + + six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6 val twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double::class.java) // 24.0 - // Division + // -- Division -- + val minusTwo = parser.parseExpression("6 / -3").getValue(Int::class.java) // -2 val one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double::class.java) // 1.0 - // Modulus + // -- Modulus -- + val three = parser.parseExpression("7 % 4").getValue(Int::class.java) // 3 - val one = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1 + val oneInt = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1 + + // -- Exponential power -- + + val maxInt = parser.parseExpression("(2^31) - 1").getValue(Int::class.java) // Integer.MAX_VALUE + + val minInt = parser.parseExpression("-2^31").getValue(Int::class.java) // Integer.MIN_VALUE + + // -- Operator precedence -- - // Operator precedence val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21 ---- ====== @@ -325,3 +529,83 @@ Kotlin:: ====== +[[expressions-operators-overloaded]] +== Overloaded Operators + +By default, the mathematical operations defined in SpEL's `Operation` enum (`ADD`, +`SUBTRACT`, `DIVIDE`, `MULTIPLY`, `MODULUS`, and `POWER`) support simple types like +numbers. By providing an implementation of `OperatorOverloader`, the expression language +can support these operations on other types. + +For example, if we want to overload the `ADD` operator to allow two lists to be +concatenated using the `+` sign, we can implement a custom `OperatorOverloader` as +follows. + +[source,java,indent=0,subs="verbatim,quotes",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)); + } + } +---- + +If we register `ListConcatenation` as the `OperatorOverloader` in a +`StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}` +as demonstrated in the following example. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setOperatorOverloader(new ListConcatenation()); + + // evaluates to a new list: [1, 2, 3, 4, 5] + parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + StandardEvaluationContext context = StandardEvaluationContext() + context.setOperatorOverloader(ListConcatenation()) + + // evaluates to a new list: [1, 2, 3, 4, 5] + parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List::class.java) +---- +====== + +[NOTE] +==== +An `OperatorOverloader` does not change the default semantics for an operator. For +example, `2 + 2` in the above example still evaluates to `4`. +==== + +[CAUTION] +==== +Any expression that uses an overloaded operator cannot be compiled. See +xref:core/expressions/evaluation.adoc#expressions-compiler-limitations[Compiler Limitations] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc index 2b4a02d24f41..d8e8b39abbe0 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc @@ -1,5 +1,5 @@ [[expressions-templating]] -= Expression templating += Expression Templating Expression templates allow mixing literal text with one or more evaluation blocks. Each evaluation block is delimited with prefix and suffix characters that you can @@ -32,53 +32,13 @@ Kotlin:: ====== The string is evaluated by concatenating the literal text `'random number is '` with the -result of evaluating the expression inside the `#{ }` delimiter (in this case, the result -of calling that `random()` method). The second argument to the `parseExpression()` method -is of the type `ParserContext`. The `ParserContext` interface is used to influence how -the expression is parsed in order to support the expression templating functionality. -The definition of `TemplateParserContext` follows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class TemplateParserContext implements ParserContext { - - public String getExpressionPrefix() { - return "#{"; - } - - public String getExpressionSuffix() { - return "}"; - } - - public boolean isTemplate() { - return true; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class TemplateParserContext : ParserContext { - - override fun getExpressionPrefix(): String { - return "#{" - } - - override fun getExpressionSuffix(): String { - return "}" - } - - override fun isTemplate(): Boolean { - return true - } - } ----- -====== +result of evaluating the expression inside the `#{ }` delimiters (in this case, the +result of calling that `random()` method). The second argument to the `parseExpression()` +method is of the type `ParserContext`. The `ParserContext` interface is used to influence +how the expression is parsed in order to support the expression templating functionality. +The `TemplateParserContext` used in the previous example resides in the +`org.springframework.expression.common` package and is an implementation of the +`ParserContext` which by default configures the prefix and suffix to `#{` and `}`, +respectively. diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc index 6f7ac6e971e9..ff285e28b584 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc @@ -1,20 +1,41 @@ [[expressions-ref-variables]] = Variables -You can reference variables in the expression by using the `#variableName` syntax. Variables -are set by using the `setVariable` method on `EvaluationContext` implementations. +You can reference variables in an expression by using the `#variableName` syntax. Variables +are set by using the `setVariable()` method in `EvaluationContext` implementations. [NOTE] ==== -Valid variable names must be composed of one or more of the following supported +Variable names must be begin with a letter (as defined below), an underscore, or a dollar +sign. + +Variable names must be composed of one or more of the following supported types of characters. -* letters: `A` to `Z` and `a` to `z` -* digits: `0` to `9` +* letter: any character for which `java.lang.Character.isLetter(char)` returns `true` + - This includes letters such as `A` to `Z`, `a` to `z`, `ü`, `ñ`, and `é` as well as + letters from other character sets such as Chinese, Japanese, Cyrillic, etc. +* digit: `0` to `9` * underscore: `_` * dollar sign: `$` ==== +[TIP] +==== +When setting a variable or root context object in the `EvaluationContext`, it is advised +that the type of the variable or root context object be `public`. + +Otherwise, certain types of SpEL expressions involving a variable or root context object +with a non-public type may fail to evaluate or compile. +==== + +[WARNING] +==== +Since variables share a common namespace with +xref:core/expressions/language-ref/functions.adoc[functions] in the evaluation context, +care must be taken to ensure that variable names and functions names do not overlap. +==== + The following example shows how to use variables. [tabs] @@ -29,7 +50,7 @@ Java:: context.setVariable("newName", "Mike Tesla"); parser.parseExpression("name = #newName").getValue(context, tesla); - System.out.println(tesla.getName()) // "Mike Tesla" + System.out.println(tesla.getName()); // "Mike Tesla" ---- Kotlin:: @@ -53,8 +74,10 @@ Kotlin:: The `#this` variable is always defined and refers to the current evaluation object (against which unqualified references are resolved). The `#root` variable is always defined and refers to the root context object. Although `#this` may vary as components of -an expression are evaluated, `#root` always refers to the root. The following examples -show how to use the `#this` and `#root` variables: +an expression are evaluated, `#root` always refers to the root. + +The following example shows how to use the `#this` variable in conjunction with +xref:core/expressions/language-ref/collection-selection.adoc[collection selection]. [tabs] ====== @@ -62,40 +85,95 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - // create an array of integers - List primes = new ArrayList<>(); - primes.addAll(Arrays.asList(2,3,5,7,11,13,17)); + // Create a list of prime integers. + List primes = List.of(2, 3, 5, 7, 11, 13, 17); - // create parser and set variable 'primes' as the array of integers + // Create parser and set variable 'primes' as the list of integers. ExpressionParser parser = new SpelExpressionParser(); - EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess(); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); context.setVariable("primes", primes); - // all prime numbers > 10 from the list (using selection ?{...}) - // evaluates to [11, 13, 17] - List primesGreaterThanTen = (List) parser.parseExpression( - "#primes.?[#this>10]").getValue(context); + // Select all prime numbers > 10 from the list (using selection ?{...}). + String expression = "#primes.?[#this > 10]"; + + // Evaluates to a list containing [11, 13, 17]. + List primesGreaterThanTen = + parser.parseExpression(expression).getValue(context, List.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // create an array of integers - val primes = ArrayList() - primes.addAll(listOf(2, 3, 5, 7, 11, 13, 17)) + // Create a list of prime integers. + val primes = listOf(2, 3, 5, 7, 11, 13, 17) - // create parser and set variable 'primes' as the array of integers + // Create parser and set variable 'primes' as the list of integers. val parser = SpelExpressionParser() - val context = SimpleEvaluationContext.forReadOnlyDataAccess() + val context = SimpleEvaluationContext.forReadWriteDataBinding().build() context.setVariable("primes", primes) - // all prime numbers > 10 from the list (using selection ?{...}) - // evaluates to [11, 13, 17] - val primesGreaterThanTen = parser.parseExpression( - "#primes.?[#this>10]").getValue(context) as List + // Select all prime numbers > 10 from the list (using selection ?{...}). + val expression = "#primes.?[#this > 10]" + + // Evaluates to a list containing [11, 13, 17]. + val primesGreaterThanTen = parser.parseExpression(expression) + .getValue(context) as List ---- ====== +The following example shows how to use the `#this` and `#root` variables together in +conjunction with +xref:core/expressions/language-ref/collection-projection.adoc[collection projection]. +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + // Create parser and evaluation context. + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + + // Create an inventor to use as the root context object. + Inventor tesla = new Inventor("Nikola Tesla"); + tesla.setInventions("Telephone repeater", "Tesla coil transformer"); + + // Iterate over all inventions of the Inventor referenced as the #root + // object, and generate a list of strings whose contents take the form + // " invented the ." (using projection !{...}). + String expression = "#root.inventions.![#root.name + ' invented the ' + #this + '.']"; + + // Evaluates to a list containing: + // "Nikola Tesla invented the Telephone repeater." + // "Nikola Tesla invented the Tesla coil transformer." + List results = parser.parseExpression(expression) + .getValue(context, tesla, List.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + // Create parser and evaluation context. + val parser = SpelExpressionParser() + val context = SimpleEvaluationContext.forReadWriteDataBinding().build() + + // Create an inventor to use as the root context object. + val tesla = Inventor("Nikola Tesla") + tesla.setInventions("Telephone repeater", "Tesla coil transformer") + + // Iterate over all inventions of the Inventor referenced as the #root + // object, and generate a list of strings whose contents take the form + // " invented the ." (using projection !{...}). + val expression = "#root.inventions.![#root.name + ' invented the ' + #this + '.']" + + // Evaluates to a list containing: + // "Nikola Tesla invented the Telephone repeater." + // "Nikola Tesla invented the Tesla coil transformer." + val results = parser.parseExpression(expression) + .getValue(context, tesla, List::class.java) +---- +====== diff --git a/framework-docs/modules/ROOT/pages/core/null-safety.adoc b/framework-docs/modules/ROOT/pages/core/null-safety.adoc index e43ad7d0f41b..8e2fe8ed42be 100644 --- a/framework-docs/modules/ROOT/pages/core/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/core/null-safety.adoc @@ -5,14 +5,14 @@ Although Java does not let you express null-safety with its type system, the Spr provides the following annotations in the `org.springframework.lang` package to let you declare nullability of APIs and fields: -* {api-spring-framework}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a +* {spring-framework-api}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a specific parameter, return value, or field can be `null`. -* {api-spring-framework}/lang/NonNull.html[`@NonNull`]: Annotation to indicate that a specific +* {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). -* {api-spring-framework}/lang/NonNullApi.html[`@NonNullApi`]: Annotation at the package level +* {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. -* {api-spring-framework}/lang/NonNullFields.html[`@NonNullFields`]: Annotation at the package +* {spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`]: Annotation at the package level that declares non-null as the default semantics for fields. The Spring Framework itself leverages these annotations, but they can also be used in any @@ -37,7 +37,7 @@ these annotations can be used by an IDE (such as IDEA or Eclipse) to provide use warnings related to null-safety in order to avoid `NullPointerException` at runtime. They are also used to make Spring APIs null-safe in Kotlin projects, since Kotlin natively -supports https://kotlinlang.org/docs/null-safety.html[null-safety]. More details +supports {kotlin-docs}/null-safety.html[null-safety]. More details are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support documentation]. @@ -46,7 +46,7 @@ are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support docum [[jsr-305-meta-annotations]] == JSR-305 meta-annotations -Spring annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=305[JSR 305] +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. diff --git a/framework-docs/modules/ROOT/pages/core/resources.adoc b/framework-docs/modules/ROOT/pages/core/resources.adoc index 3bfc74aeb33f..5fd279ab5c66 100644 --- a/framework-docs/modules/ROOT/pages/core/resources.adoc +++ b/framework-docs/modules/ROOT/pages/core/resources.adoc @@ -37,7 +37,7 @@ such as a method to check for the existence of the resource being pointed to. Spring's `Resource` interface located in the `org.springframework.core.io.` package is meant to be a more capable interface for abstracting access to low-level resources. The following listing provides an overview of the `Resource` interface. See the -{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc for further details. +{spring-framework-api}/core/io/Resource.html[`Resource`] javadoc for further details. [source,java,indent=0,subs="verbatim,quotes"] @@ -104,7 +104,7 @@ resource (if the underlying implementation is compatible and supports that functionality). Some implementations of the `Resource` interface also implement the extended -{api-spring-framework}/core/io/WritableResource.html[`WritableResource`] interface +{spring-framework-api}/core/io/WritableResource.html[`WritableResource`] interface for a resource that supports writing to it. Spring itself uses the `Resource` abstraction extensively, as an argument type in @@ -143,7 +143,7 @@ Spring includes several built-in `Resource` implementations: For a complete list of `Resource` implementations available in Spring, consult the "All Known Implementing Classes" section of the -{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc. +{spring-framework-api}/core/io/Resource.html[`Resource`] javadoc. @@ -763,7 +763,7 @@ Kotlin:: ---- ====== -See the {api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] +See the {spring-framework-api}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] javadoc for details on the various constructors. @@ -903,7 +903,7 @@ entries in the classpath. When you build JARs with Ant, do not activate the `fil switch of the JAR task. Also, classpath directories may not get exposed based on security policies in some environments -- for example, stand-alone applications on JDK 1.7.0_45 and higher (which requires 'Trusted-Library' to be set up in your manifests. See -https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources). +{stackoverflow-questions}/19394570/java-jre-7u45-breaks-classloader-getresources). On JDK 9's module path (Jigsaw), Spring's classpath scanning generally works as expected. Putting resources into a dedicated directory is highly recommendable here as well, diff --git a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc b/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc index 768af0c34345..67e9d1d31756 100644 --- a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc +++ b/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc @@ -9,7 +9,7 @@ known as _JUL_ or `java.util.logging`) if neither Log4j 2.x nor SLF4J is availab Put Log4j 2.x or Logback (or another SLF4J provider) in your classpath, without any extra bridges, and let the framework auto-adapt to your choice. For further information see the -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-logging[Spring +{spring-boot-docs}/features.html#features.logging[Spring Boot Logging Reference Documentation]. [NOTE] diff --git a/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc b/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc index db58aaa11e3b..a68dbf43f8ac 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc @@ -1,12 +1,57 @@ +[[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]] -= Bean Manipulation and the `BeanWrapper` +== 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 -https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html[javabeans]. +{java-api}/java.desktop/java/beans/package-summary.html[javabeans]. One quite important class in the beans package is the `BeanWrapper` interface and its corresponding implementation (`BeanWrapperImpl`). As quoted from the javadoc, the @@ -26,7 +71,7 @@ perform actions on that bean, such as setting and retrieving properties. [[beans-beans-conventions]] -== Setting and Getting Basic and Nested Properties +=== 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 @@ -192,7 +237,7 @@ Kotlin:: [[beans-beans-conversion]] -== Built-in `PropertyEditor` Implementations +== ``PropertyEditor``'s Spring uses the concept of a `PropertyEditor` to effect the conversion between an `Object` and a `String`. It can be handy @@ -204,7 +249,7 @@ behavior can be achieved by registering custom editors of type `java.beans.PropertyEditor`. Registering custom editors on a `BeanWrapper` or, alternatively, in a specific IoC container (as mentioned in the previous chapter), gives it the knowledge of how to convert properties to the desired type. For more about -`PropertyEditor`, see https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html[the javadoc of the `java.beans` package from Oracle]. +`PropertyEditor`, see {java-api}/java.desktop/java/beans/package-summary.html[the javadoc of the `java.beans` package from Oracle]. A couple of examples where property editing is used in Spring: @@ -310,7 +355,7 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +{java-tutorial}/javabeans/advanced/customization.html[here]). The following example uses the `BeanInfo` mechanism to explicitly register one or more `PropertyEditor` instances with the properties of an associated class: @@ -378,7 +423,7 @@ Kotlin:: [[beans-beans-conversion-customeditor-registration]] -=== Registering Additional Custom `PropertyEditor` Implementations +=== 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 @@ -521,7 +566,7 @@ Finally, the following example shows how to use `CustomEditorConfigurer` to regi ---- [[beans-beans-conversion-customeditor-registration-per]] -==== Using `PropertyEditorRegistrar` +=== `PropertyEditorRegistrar` Another mechanism for registering property editors with the Spring container is to create and use a `PropertyEditorRegistrar`. This interface is particularly useful when diff --git a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc index 3837d50a355c..7e64b0ec2877 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc @@ -2,7 +2,7 @@ = Java Bean Validation The Spring Framework provides support for the -https://beanvalidation.org/[Java Bean Validation] API. +{bean-validation-site}[Java Bean Validation] API. @@ -72,7 +72,7 @@ Kotlin:: ====== A Bean Validation validator then validates instances of this class based on the declared -constraints. See https://beanvalidation.org/[Bean Validation] for general information about +constraints. See {bean-validation-site}[Bean Validation] for general information about the API. See the https://hibernate.org/validator/[Hibernate Validator] documentation for specific constraints. To learn how to set up a bean validation provider as a Spring bean, keep reading. @@ -123,15 +123,12 @@ Validator, is expected to be present in the classpath and is automatically detec [[validation-beanvalidation-spring-inject]] -=== Injecting a Validator +=== Inject Jakarta Validator `LocalValidatorFactoryBean` implements both `jakarta.validation.ValidatorFactory` and -`jakarta.validation.Validator`, as well as Spring's `org.springframework.validation.Validator`. -You can inject a reference to either of these interfaces into beans that need to invoke -validation logic. - -You can inject a reference to `jakarta.validation.Validator` if you prefer to work with the Bean -Validation API directly, as the following example shows: +`jakarta.validation.Validator`, so you can inject a reference to the latter to +apply validation logic if you prefer to work with the Bean Validation API directly, +as the following example shows: [tabs] ====== @@ -160,8 +157,15 @@ Kotlin:: ---- ====== -You can inject a reference to `org.springframework.validation.Validator` if your bean -requires the Spring Validation API, as the following example shows: + +[[validation-beanvalidation-spring-inject-adapter]] +=== Inject Spring Validator + +In addition to implementing `jakarta.validation.Validator`, `LocalValidatorFactoryBean` +also adapts to `org.springframework.validation.Validator`, so you can inject a reference +to the latter if your bean requires the Spring Validation API. + +For example: [tabs] ====== @@ -190,9 +194,15 @@ 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 +passed into the `validate` method. + + [[validation-beanvalidation-spring-constraints]] -=== Configuring Custom Constraints +=== Configure Custom Constraints Each bean validation constraint consists of two parts: @@ -272,11 +282,10 @@ As the preceding example shows, a `ConstraintValidator` implementation can have [[validation-beanvalidation-spring-method]] -=== Spring-driven Method Validation +== Spring-driven Method Validation -You can integrate the method validation feature supported by Bean Validation 1.1 (and, as -a custom extension, also by Hibernate Validator 4.3) into a Spring context through a -`MethodValidationPostProcessor` bean definition: +You can integrate the method validation feature of Bean Validation into a +Spring context through a `MethodValidationPostProcessor` bean definition: [tabs] ====== @@ -290,7 +299,7 @@ Java:: public class AppConfig { @Bean - public MethodValidationPostProcessor validationPostProcessor() { + public static MethodValidationPostProcessor validationPostProcessor() { return new MethodValidationPostProcessor(); } } @@ -305,11 +314,11 @@ XML:: ---- ====== -To be eligible for Spring-driven method validation, all target classes need to be annotated +To be eligible for Spring-driven method validation, target classes need to be annotated with Spring's `@Validated` annotation, which can optionally also declare the validation groups to use. See -{api-spring-framework}/validation/beanvalidation/MethodValidationPostProcessor.html[`MethodValidationPostProcessor`] -for setup details with the Hibernate Validator and Bean Validation 1.1 providers. +{spring-framework-api}/validation/beanvalidation/MethodValidationPostProcessor.html[`MethodValidationPostProcessor`] +for setup details with the Hibernate Validator and Bean Validation providers. [TIP] ==== @@ -320,7 +329,141 @@ xref:core/aop/proxying.adoc#aop-understanding-aop-proxies[Understanding AOP Prox to always use methods and accessors on proxied classes; direct field access will not work. ==== +Spring MVC and WebFlux have built-in support for the same underlying method validation but without +the need for AOP. Therefore, do check the rest of this section, and also see the Spring MVC +xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation] and +xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] sections, and the WebFlux +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 `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 static MethodValidationPostProcessor validationPostProcessor() { + MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); + processor.setAdaptConstraintViolations(true); + return processor; + } + } + +---- + +XML:: ++ +[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] +---- + + + +---- +====== + +`MethodValidationException` contains a list of ``ParameterValidationResult``s which +group errors by method parameter, and each exposes a `MethodParameter`, the argument +value, and a list of `MessageSourceResolvable` errors adapted from +``ConstraintViolation``s. For `@Valid` method parameters with cascaded violations on +fields and properties, the `ParameterValidationResult` is `ParameterErrors` which +implements `org.springframework.validation.Errors` and exposes validation errors as +``FieldError``s. + + +[[validation-beanvalidation-spring-method-i18n]] +=== Customizing Validation Errors + +The adapted `MessageSourceResolvable` errors can be turned into error messages to +display to users through the configured `MessageSource` with locale and language specific +resource bundles. This section provides an example for illustration. + +Given the following class declarations: +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + record Person(@Size(min = 1, max = 10) String name) { + } + + @Validated + public class MyService { + + void addStudent(@Valid Person person, @Max(2) int degrees) { + // ... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @JvmRecord + internal data class Person(@Size(min = 1, max = 10) val name: String) + + @Validated + class MyService { + + fun addStudent(person: @Valid Person?, degrees: @Max(2) Int) { + // ... + } + } +---- +====== + +A `ConstraintViolation` on `Person.name()` is adapted to a `FieldError` with the following: + +- Error codes `"Size.person.name"`, `"Size.name"`, `"Size.java.lang.String"`, and `"Size"` +- Message arguments `"name"`, `10`, and `1` (the field name and the constraint attributes) +- Default message "size must be between 1 and 10" + +To customize the default message, you can add properties to +xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource] +resource bundles using any of the above errors codes and message arguments. Note also that the +message argument `"name"` is itself a `MessagreSourceResolvable` with error codes +`"person.name"` and `"name"` and can customized too. For example: + +Properties:: ++ +[source,properties,indent=0,subs="verbatim,quotes",role="secondary"] +---- +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) +- 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"] +---- +Max.degrees=You cannot provide more than {1} {0} +---- [[validation-beanvalidation-spring-other]] @@ -329,7 +472,7 @@ to always use methods and accessors on proxied classes; direct field access will The default `LocalValidatorFactoryBean` configuration suffices for most cases. There are a number of configuration options for various Bean Validation constructs, from message interpolation to traversal resolution. See the -{api-spring-framework}/validation/beanvalidation/LocalValidatorFactoryBean.html[`LocalValidatorFactoryBean`] +{spring-framework-api}/validation/beanvalidation/LocalValidatorFactoryBean.html[`LocalValidatorFactoryBean`] javadoc for more information on these options. diff --git a/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc b/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc index 49deddde0c33..37c62169572f 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc @@ -19,8 +19,8 @@ of the field). This is done as a convenience to aid developers when targeting er More information on the `MessageCodesResolver` and the default strategy can be found in the javadoc of -{api-spring-framework}/validation/MessageCodesResolver.html[`MessageCodesResolver`] and -{api-spring-framework}/validation/DefaultMessageCodesResolver.html[`DefaultMessageCodesResolver`], +{spring-framework-api}/validation/MessageCodesResolver.html[`MessageCodesResolver`] and +{spring-framework-api}/validation/DefaultMessageCodesResolver.html[`DefaultMessageCodesResolver`], respectively. diff --git a/framework-docs/modules/ROOT/pages/core/validation/format.adoc b/framework-docs/modules/ROOT/pages/core/validation/format.adoc index 920e2a44d970..4ac313d3f298 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/format.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/format.adoc @@ -139,7 +139,7 @@ Kotlin:: ====== The Spring team welcomes community-driven `Formatter` contributions. See -https://github.com/spring-projects/spring-framework/issues[GitHub Issues] to contribute. +{spring-framework-issues}[GitHub Issues] to contribute. diff --git a/framework-docs/modules/ROOT/pages/core/validation/validator.adoc b/framework-docs/modules/ROOT/pages/core/validation/validator.adoc index f5140480e8e1..17fa6402d3ba 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/validator.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/validator.adoc @@ -96,7 +96,7 @@ Kotlin:: The `static` `rejectIfEmpty(..)` method on the `ValidationUtils` class is used to reject the `name` property if it is `null` or the empty string. Have a look at the -{api-spring-framework}/validation/ValidationUtils.html[`ValidationUtils`] javadoc +{spring-framework-api}/validation/ValidationUtils.html[`ValidationUtils`] javadoc to see what functionality it provides besides the example shown previously. While it is certainly possible to implement a single `Validator` class to validate each @@ -193,8 +193,14 @@ Kotlin:: Validation errors are reported to the `Errors` object passed to the validator. In the case of Spring Web MVC, you can use the `` tag to inspect the error messages, but you can also inspect the `Errors` object yourself. More information about the -methods it offers can be found in the {api-spring-framework}/validation/Errors.html[javadoc]. - +methods it offers can be found in the {spring-framework-api}/validation/Errors.html[javadoc]. + +Validators may also get locally invoked for the immediate validation of a given object, +not involving a binding process. As of 6.1, this has been simplified through a new +`Validator.validateObject(Object)` method which is available by default now, returning +a simple ´Errors` representation which can be inspected: typically calling `hasErrors()` +or the new `failOnError` method for turning the error summary message into an exception +(e.g. `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`). 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..83be3072a8ef 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc @@ -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 e.g. 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. ==== 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 ebf3f4be1395..863b8f1cdac4 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 @@ -2,30 +2,26 @@ = Choosing an Approach for JDBC Database Access You can choose among several approaches to form the basis for your JDBC database access. -In addition to three flavors of `JdbcTemplate`, a new `SimpleJdbcInsert` and -`SimpleJdbcCall` approach optimizes database metadata, and the RDBMS Object style takes a -more object-oriented approach similar to that of JDO Query design. Once you start using -one of these approaches, you can still mix and match to include a feature from a -different approach. All approaches require a JDBC 2.0-compliant driver, and some -advanced features require a JDBC 3.0 driver. +In addition to three flavors of `JdbcTemplate`, a `SimpleJdbcInsert` and `SimpleJdbcCall` +approach optimizes database metadata, and the RDBMS Object style results in a more +object-oriented approach. Once you start using one of these approaches, you can still mix +and match to include a feature from a different approach. * `JdbcTemplate` is the classic and most popular Spring JDBC approach. This - "`lowest-level`" approach and all others use a JdbcTemplate under the covers. + "`lowest-level`" approach and all others use a `JdbcTemplate` under the covers. * `NamedParameterJdbcTemplate` wraps a `JdbcTemplate` to provide named parameters instead of the traditional JDBC `?` placeholders. This approach provides better documentation and ease of use when you have multiple parameters for an SQL statement. * `SimpleJdbcInsert` and `SimpleJdbcCall` optimize database metadata to limit the amount - of necessary configuration. This approach simplifies coding so that you need to - provide only the name of the table or procedure and provide a map of parameters matching - the column names. This works only if the database provides adequate metadata. If the - database does not provide this metadata, you have to provide explicit - configuration of the parameters. + of necessary configuration. This approach simplifies coding so that you only need to + provide the name of the table or procedure and a map of parameters matching the column + names. This works only if the database provides adequate metadata. If the database does + not provide this metadata, you have to provide explicit configuration of the parameters. * RDBMS objects — including `MappingSqlQuery`, `SqlUpdate`, and `StoredProcedure` — require you to create reusable and thread-safe objects during initialization of your - data-access layer. This approach is modeled after JDO Query, wherein you 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. + 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 7ebb4a44a68f..a448b8121f44 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc @@ -199,7 +199,7 @@ participating in Spring managed transactions. It is generally preferable to writ own new code by using the higher level abstractions for resource management, such as `JdbcTemplate` or `DataSourceUtils`. -See the {api-spring-framework}/jdbc/datasource/TransactionAwareDataSourceProxy.html[`TransactionAwareDataSourceProxy`] +See the {spring-framework-api}/jdbc/datasource/TransactionAwareDataSourceProxy.html[`TransactionAwareDataSourceProxy`] javadoc for more details. @@ -230,6 +230,19 @@ provided you stick to the required connection lookup pattern. Note that JTA does savepoints or custom isolation levels and has a different timeout mechanism but otherwise exposes similar behavior in terms of JDBC resources and JDBC commit/rollback management. +For JTA-style lazy retrieval of actual resource connections, Spring provides a +corresponding `DataSource` proxy class for the target connection pool: see +{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`]. +This is particularly useful for potentially empty transactions without actual statement +execution (never fetching an actual resource in such a scenario), and also in front of +a routing `DataSource` which means to take the transaction-synchronized read-only flag +and/or isolation level into account (e.g. `IsolationLevelDataSourceRouter`). + +`LazyConnectionDataSourceProxy` also provides special support for a read-only connection +pool to use during a read-only transaction, avoiding the overhead of switching the JDBC +Connection's read-only flag at the beginning and end of every transaction when fetching +it from the primary connection pool (which may be costly depending on the JDBC driver). + NOTE: As of 5.3, Spring provides an extended `JdbcTransactionManager` variant which adds exception translation capabilities on commit/rollback (aligned with `JdbcTemplate`). Where `DataSourceTransactionManager` will only ever throw `TransactionSystemException` diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc index bf2ba81cc76f..ce2aa6f47f72 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc @@ -6,6 +6,7 @@ including error handling. It includes the following topics: * xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate[Using `JdbcTemplate`] * xref:data-access/jdbc/core.adoc#jdbc-NamedParameterJdbcTemplate[Using `NamedParameterJdbcTemplate`] +* xref:data-access/jdbc/core.adoc#jdbc-JdbcClient[Unified JDBC Query/Update Operations: `JdbcClient`] * xref:data-access/jdbc/core.adoc#jdbc-SQLExceptionTranslator[Using `SQLExceptionTranslator`] * xref:data-access/jdbc/core.adoc#jdbc-statements-executing[Running Statements] * xref:data-access/jdbc/core.adoc#jdbc-statements-querying[Running Queries] @@ -50,7 +51,7 @@ corresponding to the fully qualified class name of the template instance (typica The following sections provide some examples of `JdbcTemplate` usage. These examples are not an exhaustive list of all of the functionality exposed by the `JdbcTemplate`. -See the attendant {api-spring-framework}/jdbc/core/JdbcTemplate.html[javadoc] for that. +See the attendant {spring-framework-api}/jdbc/core/JdbcTemplate.html[javadoc] for that. [[jdbc-JdbcTemplate-examples-query]] === Querying (`SELECT`) @@ -701,6 +702,120 @@ See also xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate-idioms[`JdbcTemplate` for guidelines on using the `NamedParameterJdbcTemplate` class in the context of an application. +[[jdbc-JdbcClient]] +== Unified JDBC Query/Update Operations: `JdbcClient` + +As of 6.1, the named parameter statements of `NamedParameterJdbcTemplate` and the positional +parameter statements of a regular `JdbcTemplate` are available through a unified client API +with a fluent interaction model. + +For example, with positional parameters: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + private JdbcClient jdbcClient = JdbcClient.create(dataSource); + + public int countOfActorsByFirstName(String firstName) { + return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?") + .param(firstName) + .query(Integer.class).single(); + } +---- + +For example, with named parameters: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + private JdbcClient jdbcClient = JdbcClient.create(dataSource); + + public int countOfActorsByFirstName(String firstName) { + return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName") + .param("firstName", firstName) + .query(Integer.class).single(); + } +---- + +`RowMapper` capabilities are available as well, with flexible result resolution: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + List actors = this.jdbcClient.sql("select first_name, last_name from t_actor") + .query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name"))) + .list(); +---- + +Instead of a custom `RowMapper`, you may also specify a class to map to. +For example, assuming that `Actor` has `firstName` and `lastName` properties +as a record class, a custom constructor, bean properties, or plain fields: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + List actors = this.jdbcClient.sql("select first_name, last_name from t_actor") + .query(Actor.class) + .list(); +---- + +With a required single object result: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?") + .param(1212L) + .query(Actor.class) + .single(); +---- + +With a `java.util.Optional` result: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + Optional actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?") + .param(1212L) + .query(Actor.class) + .optional(); +---- + +And for an update statement: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)") + .param("Leonor").param("Watling") + .update(); +---- + +Or an update statement with named parameters: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)") + .param("firstName", "Leonor").param("lastName", "Watling") + .update(); +---- + +Instead of individual named parameters, you may also specify a parameter source object – +for example, a record class, a class with bean properties, or a plain field holder which +provides `firstName` and `lastName` properties, such as the `Actor` class from above: + +[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") + .update(); +---- + +The automatic `Actor` class mapping for parameters as well as the query results above is +provided through implicit `SimplePropertySqlParameterSource` and `SimplePropertyRowMapper` +strategies which are also available for direct use. They can serve as a common replacement +for `BeanPropertySqlParameterSource` and `BeanPropertyRowMapper`/`DataClassRowMapper`, +also with `JdbcTemplate` and `NamedParameterJdbcTemplate` themselves. + +NOTE: `JdbcClient` is a flexible but simplified facade for JDBC query/update statements. +Advanced capabilities such as batch inserts and stored procedure calls typically require +extra customization: consider Spring's `SimpleJdbcInsert` and `SimpleJdbcCall` classes or +plain direct `JdbcTemplate` usage for any such capabilities not available in `JdbcClient`. + + [[jdbc-SQLExceptionTranslator]] == Using `SQLExceptionTranslator` @@ -709,7 +824,7 @@ between ``SQLException``s and Spring's own `org.springframework.dao.DataAccessEx which is agnostic in regard to data access strategy. Implementations can be generic (for example, using SQLState codes for JDBC) or proprietary (for example, using Oracle error codes) for greater precision. This exception translation mechanism is used behind the -the common `JdbcTemplate` and `JdbcTransactionManager` entry points which do not +common `JdbcTemplate` and `JdbcTransactionManager` entry points which do not propagate `SQLException` but rather `DataAccessException`. NOTE: As of 6.0, the default exception translator is `SQLExceptionSubclassTranslator`, diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc index eaa86da5f268..f9855a33c84d 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 @@ -83,7 +83,7 @@ Kotlin:: ---- ====== -See the {api-spring-framework}/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html[javadoc for `EmbeddedDatabaseBuilder`] +See the {spring-framework-api}/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html[javadoc for `EmbeddedDatabaseBuilder`] for further details on all supported options. You can also use the `EmbeddedDatabaseBuilder` to create an embedded database by using Java @@ -288,7 +288,7 @@ You can extend Spring JDBC embedded database support in two ways: connection pool to manage embedded database connections. We encourage you to contribute extensions to the Spring community at -https://github.com/spring-projects/spring-framework/issues[GitHub Issues]. +{spring-framework-issues}[GitHub Issues]. diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc index 65fd60c76e05..8941d958300f 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc @@ -112,7 +112,7 @@ Java:: this.actorMappingQuery = new ActorMappingQuery(dataSource); } - public Customer getCustomer(Long id) { + public Actor getActor(Long id) { return actorMappingQuery.findObject(id); } ---- @@ -123,11 +123,11 @@ Kotlin:: ---- 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` 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 4eba396012b4..f2b739def9b4 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/packages.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/packages.adoc @@ -20,9 +20,10 @@ xref:data-access/jdbc/connections.adoc[Controlling Database Connections] and xre * `object`: The `org.springframework.jdbc.object` package contains classes that represent RDBMS queries, updates, and stored procedures as thread-safe, reusable objects. See -xref:data-access/jdbc/object.adoc[Modeling JDBC Operations as Java Objects]. This approach is modeled by JDO, although objects returned by queries -are naturally disconnected from the database. This higher-level of JDBC abstraction -depends on the lower-level abstraction in the `org.springframework.jdbc.core` package. +xref:data-access/jdbc/object.adoc[Modeling JDBC Operations as Java Objects]. This style +results in a more object-oriented approach, although objects returned by queries are +naturally disconnected from the database. This higher-level of JDBC abstraction depends +on the lower-level abstraction in the `org.springframework.jdbc.core` package. * `support`: The `org.springframework.jdbc.support` package provides `SQLException` translation functionality and some utility classes. Exceptions thrown during JDBC processing 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..f7df5a755f5f 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 @@ -209,141 +209,26 @@ are passed in as a parameter to the stored procedure. The `SqlReturnType` interface has a single method (named `getTypeValue`) that must be implemented. This interface is used as part of the declaration of an `SqlOutParameter`. -The following example shows returning the value of an Oracle `STRUCT` object of the user +The following example shows returning the value of a `java.sql.Struct` object of the user declared type `ITEM_TYPE`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class TestItemStoredProcedure extends StoredProcedure { - - public TestItemStoredProcedure(DataSource dataSource) { - // ... - declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE", - (CallableStatement cs, int colIndx, int sqlType, String typeName) -> { - STRUCT struct = (STRUCT) cs.getObject(colIndx); - Object[] attr = struct.getAttributes(); - TestItem item = new TestItem(); - item.setId(((Number) attr[0]).longValue()); - item.setDescription((String) attr[1]); - item.setExpirationDate((java.util.Date) attr[2]); - return item; - })); - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { - - init { - // ... - declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName -> - val struct = cs.getObject(colIndx) as STRUCT - val attr = struct.getAttributes() - TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date) - }) - // ... - } - } ----- -====== +include-code::./TestItemStoredProcedure[] You can use `SqlTypeValue` to pass the value of a Java object (such as `TestItem`) to a stored procedure. The `SqlTypeValue` interface has a single method (named `createTypeValue`) that you must implement. The active connection is passed in, and you -can use it to create database-specific objects, such as `StructDescriptor` instances -or `ArrayDescriptor` instances. The following example creates a `StructDescriptor` instance: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - final TestItem testItem = new TestItem(123L, "A test item", - new SimpleDateFormat("yyyy-M-d").parse("2010-12-31")); - - SqlTypeValue value = new AbstractSqlTypeValue() { - protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { - StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn); - Struct item = new STRUCT(itemDescriptor, conn, - new Object[] { - testItem.getId(), - testItem.getDescription(), - new java.sql.Date(testItem.getExpirationDate().getTime()) - }); - return item; - } - }; ----- +can use it to create database-specific objects, such as `java.sql.Struct` instances +or `java.sql.Array` instances. The following example creates a `java.sql.Struct` instance: -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val (id, description, expirationDate) = TestItem(123L, "A test item", - SimpleDateFormat("yyyy-M-d").parse("2010-12-31")) - - val value = object : AbstractSqlTypeValue() { - override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { - val itemDescriptor = StructDescriptor(typeName, conn) - return STRUCT(itemDescriptor, conn, - arrayOf(id, description, java.sql.Date(expirationDate.time))) - } - } ----- -====== +include-code::./SqlTypeValueFactory[tag=struct,indent=0] You can now add this `SqlTypeValue` to the `Map` that contains the input parameters for the `execute` call of the stored procedure. Another use for the `SqlTypeValue` is passing in an array of values to an Oracle stored -procedure. Oracle has its own internal `ARRAY` class that must be used in this case, and -you can use the `SqlTypeValue` to create an instance of the Oracle `ARRAY` and populate -it with values from the Java `ARRAY`, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - final Long[] ids = new Long[] {1L, 2L}; - - SqlTypeValue value = new AbstractSqlTypeValue() { - protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { - ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn); - ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids); - return idArray; - } - }; ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { - - init { - val ids = arrayOf(1L, 2L) - val value = object : AbstractSqlTypeValue() { - override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { - val arrayDescriptor = ArrayDescriptor(typeName, conn) - return ARRAY(arrayDescriptor, conn, ids) - } - } - } - } ----- -====== - +procedure. Oracle has an `createOracleArray` method on `OracleConnection` that you can +access by unwrapping it. You can use the `SqlTypeValue` to create an array and populate +it with values from the Java `java.sql.Array`, as the following example shows: +include-code::./SqlTypeValueFactory[tag=oracle-array,indent=0] diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc index 9e0c0200a179..177249aa3521 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc @@ -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"] ---- diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc index a782b9165e01..45328b85cd86 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc @@ -407,6 +407,12 @@ exposes the Hibernate transaction as a JDBC transaction if you have set up the p `DataSource` for which the transactions are supposed to be exposed through the `dataSource` property of the `HibernateTransactionManager` class. +For JTA-style lazy retrieval of actual resource connections, Spring provides a +corresponding `DataSource` proxy class for the target connection pool: see +{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`]. +This is particularly useful for Hibernate read-only transactions which can often +be processed from a local cache rather than hitting the database. + [[orm-hibernate-resources]] == Comparing Container-managed and Locally Defined Resources diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc index bfb3e3c532ac..d44aca0c20d7 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc @@ -58,8 +58,8 @@ The benefits of using the Spring Framework to create your ORM DAOs include: TIP: For more comprehensive ORM support, including support for alternative database technologies such as MongoDB, you might want to check out the -https://projects.spring.io/spring-data/[Spring Data] suite of projects. If you are -a JPA user, the https://spring.io/guides/gs/accessing-data-jpa/[Getting Started Accessing +{spring-site-projects}/spring-data/[Spring Data] suite of projects. If you are +a JPA user, the {spring-site-guides}/gs/accessing-data-jpa/[Getting Started Accessing Data with JPA] guide from https://spring.io provides a great introduction. diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc index b9fc4279fc3f..b2400b9e16ec 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc @@ -157,7 +157,7 @@ The `LoadTimeWeaver` interface is a Spring-provided class that lets JPA `ClassTransformer` instances be plugged in a specific manner, depending on whether the environment is a web container or application server. Hooking `ClassTransformers` through an -https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html[agent] +{java-api}/java.instrument/java/lang/instrument/package-summary.html[agent] is typically not efficient. The agents work against the entire virtual machine and inspect every class that is loaded, which is usually undesirable in a production server environment. @@ -268,8 +268,8 @@ The actual JPA provider bootstrapping is handed off to the specified executor an running in parallel, to the application bootstrap thread. The exposed `EntityManagerFactory` proxy can be injected into other application components and is even able to respond to `EntityManagerFactoryInfo` configuration inspection. However, once the actual JPA provider -is being accessed by other components (for example, calling `createEntityManager`), those calls -block until the background bootstrapping has completed. In particular, when you use +is being accessed by other components (for example, calling `createEntityManager`), those +calls block until the background bootstrapping has completed. In particular, when you use Spring Data JPA, make sure to set up deferred bootstrapping for its repositories as well. @@ -284,9 +284,9 @@ to a newly created `EntityManager` per operation, in effect making its usage thr It is possible to write code against the plain JPA without any Spring dependencies, by using an injected `EntityManagerFactory` or `EntityManager`. Spring can understand the -`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method level -if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example shows a plain -JPA DAO implementation that uses the `@PersistenceUnit` annotation: +`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method +level if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example +shows a plain JPA DAO implementation that uses the `@PersistenceUnit` annotation: [tabs] ====== @@ -506,13 +506,20 @@ if you have not already done so, to get more detailed coverage of Spring's decla The recommended strategy for JPA is local transactions through JPA's native transaction support. Spring's `JpaTransactionManager` provides many capabilities known from local JDBC transactions (such as transaction-specific isolation levels and resource-level -read-only optimizations) against any regular JDBC connection pool (no XA requirement). +read-only optimizations) against any regular JDBC connection pool, without requiring +a JTA transaction coordinator and XA-capable resources. Spring JPA also lets a configured `JpaTransactionManager` expose a JPA transaction to JDBC access code that accesses the same `DataSource`, provided that the registered -`JpaDialect` supports retrieval of the underlying JDBC `Connection`. -Spring provides dialects for the EclipseLink and Hibernate JPA implementations. -See the xref:data-access/orm/jpa.adoc#orm-jpa-dialect[next section] for details on the `JpaDialect` mechanism. +`JpaDialect` supports retrieval of the underlying JDBC `Connection`. Spring provides +dialects for the EclipseLink and Hibernate JPA implementations. See the +xref:data-access/orm/jpa.adoc#orm-jpa-dialect[next section] for details on `JpaDialect`. + +For JTA-style lazy retrieval of actual resource connections, Spring provides a +corresponding `DataSource` proxy class for the target connection pool: see +{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`]. +This is particularly useful for JPA read-only transactions which can often +be processed from a local cache rather than hitting the database. [[orm-jpa-dialect]] @@ -541,8 +548,8 @@ way of auto-configuring an `EntityManagerFactory` setup for Hibernate or Eclipse respectively. Note that those provider adapters are primarily designed for use with Spring-driven transaction management (that is, for use with `JpaTransactionManager`). -See the {api-spring-framework}/orm/jpa/JpaDialect.html[`JpaDialect`] and -{api-spring-framework}/orm/jpa/JpaVendorAdapter.html[`JpaVendorAdapter`] javadoc for +See the {spring-framework-api}/orm/jpa/JpaDialect.html[`JpaDialect`] and +{spring-framework-api}/orm/jpa/JpaVendorAdapter.html[`JpaVendorAdapter`] javadoc for more details of its operations and how they are used within Spring's JPA support. diff --git a/framework-docs/modules/ROOT/pages/data-access/oxm.adoc b/framework-docs/modules/ROOT/pages/data-access/oxm.adoc index 3cbe3a502e70..5bc8fcdfda96 100644 --- a/framework-docs/modules/ROOT/pages/data-access/oxm.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/oxm.adoc @@ -36,8 +36,8 @@ simpler. [[oxm-consistent-interfaces]] === Consistent Interfaces -Spring's O-X mapping operates through two global interfaces: {api-spring-framework}/oxm/Marshaller.html[`Marshaller`] and -{api-spring-framework}/oxm/Unmarshaller.html[`Unmarshaller`]. These abstractions let you switch O-X mapping frameworks +Spring's O-X mapping operates through two global interfaces: {spring-framework-api}/oxm/Marshaller.html[`Marshaller`] and +{spring-framework-api}/oxm/Unmarshaller.html[`Unmarshaller`]. These abstractions let you switch O-X mapping frameworks with relative ease, with little or no change required on the classes that do the marshalling. This approach has the additional benefit of making it possible to do XML marshalling with a mix-and-match approach (for example, some marshalling performed using JAXB @@ -557,7 +557,7 @@ set the `supportedClasses` property on the `XStreamMarshaller`, as the following Doing so ensures that only the registered classes are eligible for unmarshalling. Additionally, you can register -{api-spring-framework}/oxm/xstream/XStreamMarshaller.html#setConverters(com.thoughtworks.xstream.converters.ConverterMatcher...)[custom +{spring-framework-api}/oxm/xstream/XStreamMarshaller.html#setConverters(com.thoughtworks.xstream.converters.ConverterMatcher...)[custom converters] to make sure that only your supported classes can be unmarshalled. You might want to add a `CatchAllConverter` as the last converter in the list, in addition to converters that explicitly support the domain classes that should be supported. As a diff --git a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc index 453e9c14ce11..672f396ef0f9 100644 --- a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc @@ -95,7 +95,7 @@ parameter to database bind marker translation. run. * `….namedParameters(false)`: Disable named parameter expansion. Enabled by default. -TIP: Dialects are resolved by {api-spring-framework}/r2dbc/core/binding/BindMarkersFactoryResolver.html[`BindMarkersFactoryResolver`] +TIP: Dialects are resolved by {spring-framework-api}/r2dbc/core/binding/BindMarkersFactoryResolver.html[`BindMarkersFactoryResolver`] from a `ConnectionFactory`, typically by inspecting `ConnectionFactoryMetadata`. + You can let Spring auto-discover your `BindMarkersFactory` by registering a @@ -120,7 +120,7 @@ the reactive sequence to aid debugging. The following sections provide some examples of `DatabaseClient` usage. These examples are not an exhaustive list of all of the functionality exposed by the `DatabaseClient`. -See the attendant {api-spring-framework}/r2dbc/core/DatabaseClient.html[javadoc] for that. +See the attendant {spring-framework-api}/r2dbc/core/DatabaseClient.html[javadoc] for that. [[r2dbc-DatabaseClient-examples-statement]] ==== Executing Statements @@ -254,6 +254,25 @@ Kotlin:: ---- ====== +Alternatively, there is a shortcut for mapping to a single value: + +[source,java] +---- + Flux names = client.sql("SELECT name FROM person") + .mapValue(String.class) + .all(); +---- + +Or you may map to a result object with bean properties or record components: + +[source,java] +---- + // assuming a name property on Person + Flux persons = client.sql("SELECT name FROM person") + .mapProperties(Person.class) + .all(); +---- + [[r2dbc-DatabaseClient-mapping-null]] .What about `null`? **** @@ -324,6 +343,27 @@ The following example shows parameter binding for a query: .bind("age", 34); ---- +Alternatively, you may pass in a map of names and values: + +[source,java] +---- + Map params = new LinkedHashMap<>(); + params.put("id", "joe"); + params.put("name", "Joe"); + params.put("age", 34); + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bindValues(params); +---- + +Or you may pass in a parameter object with bean properties or record components: + +[source,java] +---- + // assuming id, name, age properties on Person + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bindProperties(new Person("joe", "Joe", 34); +---- + .R2DBC Native Bind Markers **** R2DBC uses database-native bind markers that depend on the actual database vendor. @@ -400,12 +440,8 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - val tuples: MutableList> = ArrayList() - tuples.add(arrayOf("John", 35)) - tuples.add(arrayOf("Ann", 50)) - client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("tuples", arrayOf(35, 50)) + .bind("ages", arrayOf(35, 50)) ---- ====== @@ -712,7 +748,7 @@ the same time, have this client participating in Spring managed transactions. It preferable to integrate a R2DBC client with proper access to `ConnectionFactoryUtils` for resource management. -See the {api-spring-framework}/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html[`TransactionAwareConnectionFactoryProxy`] +See the {spring-framework-api}/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html[`TransactionAwareConnectionFactoryProxy`] javadoc for more details. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc index eb94d9516959..4e865292cdd4 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc @@ -7,7 +7,7 @@ the JTA `UserTransaction` and `TransactionManager` objects) autodetects the loca the latter object, which varies by application server. Having access to the JTA `TransactionManager` allows for enhanced transaction semantics -- in particular, supporting transaction suspension. See the -{api-spring-framework}/transaction/jta/JtaTransactionManager.html[`JtaTransactionManager`] +{spring-framework-api}/transaction/jta/JtaTransactionManager.html[`JtaTransactionManager`] javadoc for details. Spring's `JtaTransactionManager` is the standard choice to run on Jakarta EE application diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc index 2ce54174913c..a866ccb56436 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 @@ -74,10 +74,11 @@ Kotlin:: ---- ====== -Used at the class level as above, the annotation indicates a default for all methods of -the declaring class (as well as its subclasses). Alternatively, each method can be -annotated individually. See xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-annotations-method-visibility[method visibility] for -further details on which methods Spring considers transactional. Note that a class-level +Used at the class level as above, the annotation indicates a default for all methods +of the declaring class (as well as its subclasses). Alternatively, each method can be +annotated individually. See +xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-annotations-method-visibility[method visibility] +for further details on which methods Spring considers transactional. Note that a class-level annotation does not apply to ancestor classes up the class hierarchy; in such a scenario, inherited methods need to be locally redeclared in order to participate in a subclass-level annotation. @@ -85,7 +86,7 @@ subclass-level annotation. When a POJO class such as the one above is defined as a bean in a Spring context, you can make the bean instance transactional through an `@EnableTransactionManagement` annotation in a `@Configuration` class. See the -{api-spring-framework}/transaction/annotation/EnableTransactionManagement.html[javadoc] +{spring-framework-api}/transaction/annotation/EnableTransactionManagement.html[javadoc] for full details. In XML configuration, the `` tag provides similar convenience: @@ -262,7 +263,7 @@ is modified) to support `@Transactional` runtime behavior on any kind of method. | XML Attribute| Annotation Attribute| Default| Description | `transaction-manager` -| N/A (see {api-spring-framework}/transaction/annotation/TransactionManagementConfigurer.html[`TransactionManagementConfigurer`] javadoc) +| N/A (see {spring-framework-api}/transaction/annotation/TransactionManagementConfigurer.html[`TransactionManagementConfigurer`] javadoc) | `transactionManager` | Name of the transaction manager to use. Required only if the name of the transaction manager is not `transactionManager`, as in the preceding example. @@ -436,9 +437,10 @@ properties of the `@Transactional` annotation: | Optional array of exception name patterns that must not cause rollback. |=== -TIP: See xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] for further details -on rollback rule semantics, patterns, and warnings regarding possible unintentional -matches for pattern-based rollback rules. +TIP: See +xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] +for further details on rollback rule semantics, patterns, and warnings +regarding possible unintentional matches for pattern-based rollback rules. Currently, you cannot have explicit control over the name of a transaction, where 'name' means the transaction name that appears in a transaction monitor and in logging output. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/aspectj.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/aspectj.adoc index 58a65cafff62..af4de5b40077 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 @@ -57,8 +57,7 @@ transaction semantics given by the class annotation (if present). You can annota regardless of visibility. To weave your applications with the `AnnotationTransactionAspect`, you must either build -your application with AspectJ (see the -https://www.eclipse.org/aspectj/doc/released/devguide/index.html[AspectJ Development +your application with AspectJ (see the {aspectj-docs-devguide}/index.html[AspectJ Development Guide]) or use load-time weaving. See xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time weaving with AspectJ in the Spring Framework] for a discussion of load-time weaving with AspectJ. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc index b002be989d24..dca97fd50248 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 @@ -23,9 +23,9 @@ 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'. This allows you to handle functional-style errors using Try and have the transaction automatically rolled back in case of a failure. For more information on Vavr's Try, -refer to the [official Vavr documentation](https://www.vavr.io/vavr-docs/#_try). - +refer to the {vavr-docs}/#_try[official Vavr documentation]. Here's an example of how to use Vavr's Try with a transactional method: + [tabs] ====== Java:: @@ -42,6 +42,32 @@ Java:: ---- ====== +As of Spring Framework 6.1, there is also special treatment of `CompletableFuture` +(and general `Future`) return values, triggering a rollback for such a handle if it +was exceptionally completed at the time of being returned from the original method. +This is intended for `@Async` methods where the actual method implementation may +need to comply with a `CompletableFuture` signature (auto-adapted to an actual +asynchronous handle for a call to the proxy by `@Async` processing at runtime), +preferring exposure in the returned handle rather than rethrowing an exception: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @Transactional @Async + public CompletableFuture myTransactionalMethod() { + try { + return CompletableFuture.completedFuture(delegate.myDataAccessOperation()); + } + catch (DataAccessException ex) { + return CompletableFuture.failedFuture(ex); + } + } +---- +====== + Checked exceptions that are thrown from a transactional method do not result in a rollback in the default configuration. You can configure exactly which `Exception` types mark a transaction for rollback, including checked exceptions by specifying _rollback rules_. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc index f59bb26b0005..e27f496de9fe 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc @@ -38,6 +38,11 @@ within the method. A reactive transaction managed by `ReactiveTransactionManager` uses the Reactor context instead of thread-local attributes. As a consequence, all participating data access operations need to execute within the same Reactor context in the same reactive pipeline. + +When configured with a `ReactiveTransactionManager`, all transaction-demarcated methods +are expected to return a reactive pipeline. Void methods or regular return types need +to be associated with a regular `PlatformTransactionManager`, e.g. through the +`transactionManager` attribute of the corresponding `@Transactional` declarations. ==== The following image shows a conceptual view of calling a method on a transactional proxy: diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc index cf4bceac371b..b41fd48f0d51 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc @@ -75,6 +75,6 @@ that it can roll back to. Such partial rollbacks let an inner transaction scope trigger a rollback for its scope, with the outer transaction being able to continue the physical transaction despite some operations having been rolled back. This setting is typically mapped onto JDBC savepoints, so it works only with JDBC resource -transactions. See Spring's {api-spring-framework}/jdbc/datasource/DataSourceTransactionManager.html[`DataSourceTransactionManager`]. +transactions. See Spring's {spring-framework-api}/jdbc/datasource/DataSourceTransactionManager.html[`DataSourceTransactionManager`]. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc index b4edfd8c4e21..62749a4d5842 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc @@ -57,10 +57,14 @@ attribute of the annotation to `true`. [NOTE] ==== -`@TransactionalEventListener` only works with thread-bound transactions managed by -`PlatformTransactionManager`. A reactive transaction managed by `ReactiveTransactionManager` -uses the Reactor context instead of thread-local attributes, so from the perspective of -an event listener, there is no compatible active transaction that it can participate in. +As of 6.1, `@TransactionalEventListener` can work with thread-bound transactions managed by +`PlatformTransactionManager` as well as reactive transactions managed by `ReactiveTransactionManager`. +For the former, listeners are guaranteed to see the current thread-bound transaction. +Since the latter uses the Reactor context instead of thread-local variables, the transaction +context needs to be included in the published event instance as the event source. +See the +{spring-framework-api}/transaction/reactive/TransactionalEventPublisher.html[`TransactionalEventPublisher`] +javadoc for details. ==== diff --git a/framework-docs/modules/ROOT/pages/index.adoc b/framework-docs/modules/ROOT/pages/index.adoc index 29df7928806c..6c0a08843f79 100644 --- a/framework-docs/modules/ROOT/pages/index.adoc +++ b/framework-docs/modules/ROOT/pages/index.adoc @@ -16,10 +16,10 @@ STOMP Messaging. xref:web-reactive.adoc[Web Reactive] :: Spring WebFlux, WebClient, WebSocket, RSocket. xref:integration.adoc[Integration] :: REST Clients, JMS, JCA, JMX, -Email, Tasks, Scheduling, Caching, Observability. +Email, Tasks, Scheduling, Caching, Observability, JVM Checkpoint Restore. xref:languages.adoc[Languages] :: Kotlin, Groovy, Dynamic Languages. -xref:testing/appendix.adoc[Appendix] :: Spring properties. -https://github.com/spring-projects/spring-framework/wiki[Wiki] :: What's New, +xref:appendix.adoc[Appendix] :: Spring properties. +{spring-framework-wiki}[Wiki] :: What's New, Upgrade Notes, Supported Versions, additional cross-version information. Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu, Rob Harrop, Thomas Risberg, @@ -29,8 +29,6 @@ Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clem Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch -Copyright © 2002 - 2023 VMware, Inc. All Rights Reserved. - Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each -copy contains this Copyright Notice, whether distributed in print or electronically. +copy contains the Copyright Notice, whether distributed in print or electronically. diff --git a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc index 68358c48ef2c..b25668ef5c48 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc @@ -67,7 +67,7 @@ To provide a different default key generator, you need to implement the The default key generation strategy changed with the release of Spring 4.0. Earlier versions of Spring used a key generation strategy that, for multiple key parameters, considered only the `hashCode()` of parameters and not `equals()`. This could cause -unexpected key collisions (see https://jira.spring.io/browse/SPR-10237[SPR-10237] +unexpected key collisions (see {spring-framework-issues}/14870[spring-framework#14870] for background). The new `SimpleKeyGenerator` uses a compound key for such scenarios. If you want to keep using the previous key strategy, you can configure the deprecated @@ -208,6 +208,76 @@ NOTE: This is an optional feature, and your favorite cache library may not suppo All `CacheManager` implementations provided by the core framework support it. See the documentation of your cache provider for more details. +[[cache-annotations-cacheable-reactive]] +=== Caching with CompletableFuture and Reactive Return Types + +As of 6.1, cache annotations take `CompletableFuture` and reactive return types +into account, automatically adapting the cache interaction accordingly. + +For a method returning a `CompletableFuture`, the object produced by that future +will be cached whenever it is complete, and the cache lookup for a cache hit will +be retrieved via a `CompletableFuture`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable("books") + public CompletableFuture findBook(ISBN isbn) {...} +---- + +For a method returning a Reactor `Mono`, the object emitted by that Reactive Streams +publisher will be cached whenever it is available, and the cache lookup for a cache +hit will be retrieved as a `Mono` (backed by a `CompletableFuture`): + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable("books") + public Mono findBook(ISBN isbn) {...} +---- + +For a method returning a Reactor `Flux`, the objects emitted by that Reactive Streams +publisher will be collected into a `List` and cached whenever that list is complete, +and the cache lookup for a cache hit will be retrieved as a `Flux` (backed by a +`CompletableFuture` for the cached `List` value): + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable("books") + public Flux findBooks(String author) {...} +---- + +Such `CompletableFuture` and reactive adaptation also works for synchronized caching, +computing the value only once in case of a concurrent cache miss: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Cacheable(cacheNames="foos", sync=true) <1> + public CompletableFuture executeExpensiveOperation(String id) {...} +---- +<1> Using the `sync` attribute. + +NOTE: In order for such an arrangement to work at runtime, the configured cache +needs to be capable of `CompletableFuture`-based retrieval. The Spring-provided +`ConcurrentMapCacheManager` automatically adapts to that retrieval style, and +`CaffeineCacheManager` natively supports it when its asynchronous cache mode is +enabled: set `setAsyncCacheMode(true)` on your `CaffeineCacheManager` instance. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Bean + CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCacheSpecification(...); + cacheManager.setAsyncCacheMode(true); + return cacheManager; + } +---- + +Last but not least, be aware that annotation-driven caching is not appropriate +for sophisticated reactive interactions involving composition and back pressure. +If you choose to declare `@Cacheable` on specific reactive methods, consider the +impact of the rather coarse-granular cache interaction which simply stores the +emitted object for a `Mono` or even a pre-collected list of objects for a `Flux`. + [[cache-annotations-cacheable-condition]] === Conditional Caching @@ -262,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 @@ -288,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` @@ -298,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` @@ -337,6 +408,9 @@ other), such declarations should be avoided. Note also that such conditions shou on the result object (that is, the `#result` variable), as these are validated up-front to confirm the exclusion. +As of 6.1, `@CachePut` takes `CompletableFuture` and reactive return types into account, +performing the put operation whenever the produced object is available. + [[cache-annotations-evict]] == The `@CacheEvict` Annotation @@ -379,6 +453,9 @@ trigger, the return values are ignored (as they do not interact with the cache). not the case with `@Cacheable` which adds data to the cache or updates data in the cache and, thus, requires a result. +As of 6.1, `@CacheEvict` takes `CompletableFuture` and reactive return types into account, +performing an after-invocation evict operation whenever processing has completed. + [[cache-annotations-caching]] == The `@Caching` Annotation @@ -448,7 +525,7 @@ To enable caching annotations add the annotation `@EnableCaching` to one of your ---- @Configuration @EnableCaching - public class AppConfig { + class AppConfig { @Bean CacheManager cacheManager() { @@ -490,7 +567,7 @@ switching to `aspectj` mode in combination with compile-time or load-time weavin NOTE: For more detail about advanced customizations (using Java configuration) that are required to implement `CachingConfigurer`, see the -{api-spring-framework}/cache/annotation/CachingConfigurer.html[javadoc]. +{spring-framework-api}/cache/annotation/CachingConfigurer.html[javadoc]. [[cache-annotation-driven-settings]] .Cache annotation settings @@ -499,7 +576,7 @@ required to implement `CachingConfigurer`, see the | XML Attribute | Annotation Attribute | Default | Description | `cache-manager` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | `cacheManager` | The name of the cache manager to use. A default `CacheResolver` is initialized behind the scenes with this cache manager (or `cacheManager` if not set). For more @@ -507,19 +584,19 @@ required to implement `CachingConfigurer`, see the attribute. | `cache-resolver` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | A `SimpleCacheResolver` using the configured `cacheManager`. | The bean name of the CacheResolver that is to be used to resolve the backing caches. This attribute is not required and needs to be specified only as an alternative to the 'cache-manager' attribute. | `key-generator` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | `SimpleKeyGenerator` | Name of the custom key generator to use. | `error-handler` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | `SimpleCacheErrorHandler` | The name of the custom cache error handler to use. By default, any exception thrown during a cache related operation is thrown back at the client. diff --git a/framework-docs/modules/ROOT/pages/integration/cds.adoc b/framework-docs/modules/ROOT/pages/integration/cds.adoc new file mode 100644 index 000000000000..aeffe326c10d --- /dev/null +++ b/framework-docs/modules/ROOT/pages/integration/cds.adoc @@ -0,0 +1,72 @@ +[[cds]] += CDS +:page-aliases: integration/class-data-sharing.adoc + +Class Data Sharing (CDS) is a https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[JVM feature] +that can help reduce the startup time and memory footprint of Java applications. + +To use this feature, a CDS archive should be created for the particular classpath of the +application. The Spring Framework provides a hook-point to ease the creation of the +archive. Once the archive is available, users should opt in to use it via a JVM flag. + +== Creating the CDS Archive + +A CDS archive for an application can be created when the application exits. The Spring +Framework provides a mode of operation where the process can exit automatically once the +`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons +have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been +invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet +been published. + +To create the archive, two additional JVM flags must be specified: + +* `-XX:ArchiveClassesAtExit=application.jsa`: creates the CDS archive on exit +* `-Dspring.context.exit=onRefresh`: starts and then immediately exits your Spring + application as described above + +To create a CDS archive, your JDK/JRE must have a base image. If you add the flags above to +your startup script, you may get a warning that looks like this: + +[source,shell,indent=0,subs="verbatim"] +---- + -XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info. +---- + +The base CDS archive is usually provided out-of-the-box, but can also be created if needed by issuing the following +command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -Xshare:dump +---- + +== Using the Archive + +Once the archive is available, add `-XX:SharedArchiveFile=application.jsa` to your startup +script to use it, assuming an `application.jsa` file in the working directory. + +To check if the CDS cache is effective, you can use (for testing purposes only, not in production) `-Xshare:on` which +prints an error message and exits if CDS can't be enabled. + +To figure out how effective the cache is, you can enable class loading logs by adding +an extra attribute: `-Xlog:class+load:file=cds.log`. This creates a `cds.log` with every +attempt to load a class and its source. Classes that are loaded from the cache should have +a "shared objects file" source, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + [0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top) + [0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top) + [0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top) + [0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top) + [0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top) +---- + +If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that +the following conditions are fulfilled when creating and using the archive: + + - The very same JVM must be used. + - The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters. + - The timestamps of the JARs must be preserved. + - When using the archive, the classpath must be the same than the one used to create the archive, in the same order. +Additional JARs or directories can be specified *at the end* (but won't be cached). diff --git a/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc b/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc new file mode 100644 index 000000000000..934b95b1dc85 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc @@ -0,0 +1,40 @@ +[[checkpoint-restore]] += JVM Checkpoint Restore + +The Spring Framework integrates with checkpoint/restore as implemented by https://github.com/CRaC/docs[Project CRaC] in order to allow implementing systems capable of reducing the startup and warmup times of Spring-based Java applications with the JVM. + +Using this feature requires: + +* A checkpoint/restore enabled JVM (Linux only for now). +* The presence of the https://github.com/CRaC/org.crac[`org.crac:crac`] library (version `1.4.0` and above are supported) in the classpath. +* Specifying the required `java` command-line parameters like `-XX:CRaCCheckpointTo=PATH` or `-XX:CRaCRestoreFrom=PATH`. + +WARNING: The files generated in the path specified by `-XX:CRaCCheckpointTo=PATH` when a checkpoint is requested contain a representation of the memory of the running JVM, which may contain secrets and other sensitive data. Using this feature should be done with the assumption that any value "seen" by the JVM, such as configuration properties coming from the environment, will be stored in those CRaC files. As a consequence, the security implications of where and how those files are generated, stored, and accessed should be carefully assessed. + +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 +startup during the `LifecycleProcessor.onRefresh` phase. After this phase has completed, all non-lazy initialized singletons have been instantiated, and +`InitializingBean#afterPropertiesSet` callbacks have been invoked; but the lifecycle has not started, and the +`ContextRefreshedEvent` has not yet been published. + +For testing purposes, it is also possible to leverage the `-Dspring.context.exit=onRefresh` JVM system property which +triggers similar behavior, but instead of creating a checkpoint, it exits your Spring application at the same lifecycle +phase without requiring the Project CraC dependency/JVM or Linux. This can be useful to check if connections to remote +services are required when the beans are not started, and potentially refine the configuration to avoid that. + +WARNING: As mentioned above, and especially in use cases where the CRaC files are shipped as part of a deployable artifact (a container image for example), operate with the assumption that any sensitive data "seen" by the JVM ends up in the CRaC files, and assess carefully the related security implications. + +NOTE: Automatic checkpoint/restore is a way to "fast-forward" the startup of the application to a phase where the application context is about to start, but it does not allow to have a fully warmed-up JVM. diff --git a/framework-docs/modules/ROOT/pages/integration/email.adoc b/framework-docs/modules/ROOT/pages/integration/email.adoc index 610fda7c8797..19568c212c26 100644 --- a/framework-docs/modules/ROOT/pages/integration/email.adoc +++ b/framework-docs/modules/ROOT/pages/integration/email.adoc @@ -26,7 +26,7 @@ interface. A simple value object that encapsulates the properties of a simple ma as `from` and `to` (plus many others) is the `SimpleMailMessage` class. This package also contains a hierarchy of checked exceptions that provide a higher level of abstraction over the lower level mail system exceptions, with the root exception being -`MailException`. See the {api-spring-framework}/mail/MailException.html[javadoc] +`MailException`. See the {spring-framework-api}/mail/MailException.html[javadoc] for more information on the rich mail exception hierarchy. The `org.springframework.mail.javamail.JavaMailSender` interface adds specialized @@ -85,7 +85,7 @@ email when someone places an order: // Call the collaborators to persist the order... - // Create a thread safe "copy" of the template message and customize it + // Create a thread-safe "copy" of the template message and customize it SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage); msg.setTo(order.getCustomer().getEmailAddress()); msg.setText( diff --git a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc index edda9de3d387..e2cbe5fe91d9 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc @@ -58,13 +58,13 @@ your `@Configuration` classes, as the following example shows: By default, the infrastructure looks for a bean named `jmsListenerContainerFactory` as the source for the factory to use to create message listener containers. In this case (and ignoring the JMS infrastructure setup), you can invoke the `processOrder` -method with a core poll size of three threads and a maximum pool size of ten threads. +method with a core pool size of three threads and a maximum pool size of ten threads. You can customize the listener container factory to use for each annotation or you can configure an explicit default by implementing the `JmsListenerConfigurer` interface. The default is required only if at least one endpoint is registered without a specific container factory. See the javadoc of classes that implement -{api-spring-framework}/jms/annotation/JmsListenerConfigurer.html[`JmsListenerConfigurer`] +{spring-framework-api}/jms/annotation/JmsListenerConfigurer.html[`JmsListenerConfigurer`] for details and examples. If you prefer xref:integration/jms/namespace.adoc[XML configuration], you can use the `` diff --git a/framework-docs/modules/ROOT/pages/integration/jms/jca-message-endpoint-manager.adoc b/framework-docs/modules/ROOT/pages/integration/jms/jca-message-endpoint-manager.adoc index 23ce156526d3..70942ae58dd7 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 @@ -64,9 +64,9 @@ In some environments, you can instead obtain the entire `ResourceAdapter` object (by using ``). The Spring-based message listeners can then interact with the server-hosted `ResourceAdapter`, which also use the server's built-in `WorkManager`. -See the javadoc for {api-spring-framework}/jms/listener/endpoint/JmsMessageEndpointManager.html[`JmsMessageEndpointManager`], -{api-spring-framework}/jms/listener/endpoint/JmsActivationSpecConfig.html[`JmsActivationSpecConfig`], -and {api-spring-framework}/jca/support/ResourceAdapterFactoryBean.html[`ResourceAdapterFactoryBean`] +See the javadoc for {spring-framework-api}/jms/listener/endpoint/JmsMessageEndpointManager.html[`JmsMessageEndpointManager`], +{spring-framework-api}/jms/listener/endpoint/JmsActivationSpecConfig.html[`JmsActivationSpecConfig`], +and {spring-framework-api}/jca/support/ResourceAdapterFactoryBean.html[`ResourceAdapterFactoryBean`] for more details. Spring also provides a generic JCA message endpoint manager that is not tied to JMS: @@ -74,7 +74,7 @@ Spring also provides a generic JCA message endpoint manager that is not tied to for using any message listener type (such as a JMS `MessageListener`) and any provider-specific `ActivationSpec` object. See your JCA provider's documentation to find out about the actual capabilities of your connector, and see the -{api-spring-framework}/jca/endpoint/GenericMessageEndpointManager.html[`GenericMessageEndpointManager`] +{spring-framework-api}/jca/endpoint/GenericMessageEndpointManager.html[`GenericMessageEndpointManager`] javadoc for the Spring-specific configuration details. NOTE: JCA-based message endpoint management is very analogous to EJB 2.1 Message-Driven Beans. diff --git a/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc b/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc index be6cb7b45cbd..8ecda386a372 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc @@ -113,7 +113,7 @@ as the following example shows: ---- The following table describes all available attributes. See the class-level javadoc -of the {api-spring-framework}/jms/listener/AbstractMessageListenerContainer.html[`AbstractMessageListenerContainer`] +of the {spring-framework-api}/jms/listener/AbstractMessageListenerContainer.html[`AbstractMessageListenerContainer`] and its concrete subclasses for more details on the individual properties. The javadoc also provides a discussion of transaction choices and message redelivery scenarios. @@ -254,7 +254,7 @@ The following table describes the available configuration options for the JCA va | `activation-spec-factory` | A reference to the `JmsActivationSpecFactory`. The default is to autodetect the JMS - provider and its `ActivationSpec` class (see {api-spring-framework}/jms/listener/endpoint/DefaultJmsActivationSpecFactory.html[`DefaultJmsActivationSpecFactory`]). + provider and its `ActivationSpec` class (see {spring-framework-api}/jms/listener/endpoint/DefaultJmsActivationSpecFactory.html[`DefaultJmsActivationSpecFactory`]). | `destination-resolver` | A reference to the `DestinationResolver` strategy for resolving JMS `Destinations`. diff --git a/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc b/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc index ff47edf51210..81388f3ae0f5 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc @@ -76,7 +76,7 @@ containers that ships with Spring (in this case, `DefaultMessageListenerContaine ---- See the Spring javadoc of the various message listener containers (all of which implement -{api-spring-framework}/jms/listener/MessageListenerContainer.html[MessageListenerContainer]) +{spring-framework-api}/jms/listener/MessageListenerContainer.html[MessageListenerContainer]) for a full description of the features supported by each implementation. diff --git a/framework-docs/modules/ROOT/pages/integration/jms/using.adoc b/framework-docs/modules/ROOT/pages/integration/jms/using.adoc index db342992e03e..027098cbc205 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/using.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/using.adoc @@ -234,7 +234,7 @@ use a proper cache level in such a case. This container also has recoverable capabilities when the broker goes down. By default, a simple `BackOff` implementation retries every five seconds. You can specify a custom `BackOff` implementation for more fine-grained recovery options. See -{api-spring-framework}/util/backoff/ExponentialBackOff.html[`ExponentialBackOff`] for an example. +{spring-framework-api}/util/backoff/ExponentialBackOff.html[`ExponentialBackOff`] for an example. NOTE: Like its sibling (xref:integration/jms/using.adoc#jms-mdp-simple[`SimpleMessageListenerContainer`]), `DefaultMessageListenerContainer` supports native JMS transactions and allows for diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc index 78e197acbbb4..4811434a9c70 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc @@ -246,7 +246,7 @@ instance. The `NotificationPublisherAware` interface supplies an instance of a which the bean can then use to publish `Notifications`. As stated in the javadoc of the -{api-spring-framework}/jmx/export/notification/NotificationPublisher.html[`NotificationPublisher`] +{spring-framework-api}/jmx/export/notification/NotificationPublisher.html[`NotificationPublisher`] interface, managed beans that publish events through the `NotificationPublisher` mechanism are not responsible for the state management of notification listeners. Spring's JMX support takes care of handling all the JMX infrastructure issues. diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc index 369ccbf2c19c..7e6164bc86e6 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc @@ -6,10 +6,8 @@ This section contains links to further resources about JMX: * The https://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html[JMX homepage] at Oracle. -* The https://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html[JMX - specification] (JSR-000003). -* The https://jcp.org/aboutJava/communityprocess/final/jsr160/index.html[JMX Remote API - specification] (JSR-000160). +* The {JSR}003[JMX specification] (JSR-000003). +* The {JSR}160[JMX Remote API specification] (JSR-000160). * The http://mx4j.sourceforge.net/[MX4J homepage]. (MX4J is an open-source implementation of various JMX specs.) diff --git a/framework-docs/modules/ROOT/pages/integration/observability.adoc b/framework-docs/modules/ROOT/pages/integration/observability.adoc index c2072e23f83c..28fe88fdbc3c 100644 --- a/framework-docs/modules/ROOT/pages/integration/observability.adoc +++ b/framework-docs/modules/ROOT/pages/integration/observability.adoc @@ -1,13 +1,13 @@ [[observability]] = Observability Support -Micrometer defines an https://micrometer.io/docs/observation[Observation concept that enables both Metrics and Traces] in applications. +Micrometer defines an {micrometer-docs}/observation.html[Observation concept that enables both Metrics and Traces] in applications. Metrics support offers a way to create timers, gauges, or counters for collecting statistics about the runtime behavior of your application. Metrics can help you to track error rates, usage patterns, performance, and more. Traces provide a holistic view of an entire system, crossing application boundaries; you can zoom in on particular user requests and follow their entire completion across applications. Spring Framework instruments various parts of its own codebase to publish observations if an `ObservationRegistry` is configured. -You can learn more about {docs-spring-boot}/html/actuator.html#actuator.metrics[configuring the observability infrastructure in Spring Boot]. +You can learn more about {spring-boot-docs}/actuator.html#actuator.metrics[configuring the observability infrastructure in Spring Boot]. [[observability.list]] @@ -21,15 +21,24 @@ As outlined xref:integration/observability.adoc[at the beginning of this section |=== |Observation name |Description -|xref:integration/observability.adoc#http-client[`"http.client.requests"`] +|xref:integration/observability.adoc#observability.http-client[`"http.client.requests"`] |Time spent for HTTP client exchanges -|xref:integration/observability.adoc#http-server[`"http.server.requests"`] +|xref:integration/observability.adoc#observability.http-server[`"http.server.requests"`] |Processing time for HTTP server exchanges at the Framework level + +|xref:integration/observability.adoc#observability.jms.publish[`"jms.message.publish"`] +|Time spent sending a JMS message to a destination by a message producer. + +|xref:integration/observability.adoc#observability.jms.process[`"jms.message.process"`] +|Processing time for a JMS message that was previously received by a message consumer. + +|xref:integration/observability.adoc#observability.tasks-scheduled[`"tasks.scheduled.execution"`] +|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] +{micrometer-docs}/concepts/naming.html[to the format preferred by the monitoring system backend] (Prometheus, Atlas, Graphite, InfluxDB...). @@ -79,6 +88,101 @@ include-code::./ServerRequestObservationFilter[] You can configure `ObservationFilter` instances on the `ObservationRegistry`. +[[observability.tasks-scheduled]] +== @Scheduled tasks instrumentation + +An Observation is created for xref:integration/scheduling.adoc#scheduling-enable-annotation-support[each execution of an `@Scheduled` task]. +Applications need to configure the `ObservationRegistry` on the `ScheduledTaskRegistrar` to enable the recording of observations. +This can be done by declaring a `SchedulingConfigurer` bean that sets the observation registry: + +include-code::./ObservationSchedulingConfigurer[] + +It is using 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). + +By default, the following `KeyValues` are created: + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`code.function` _(required)_|Name of Java `Method` that is scheduled for execution. +|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method, or `"ANONYMOUS"` for anonymous classes. +|`error` _(required)_|Class name of the exception thrown during the execution, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. +|`outcome` _(required)_|Outcome of the method execution. Can be `"SUCCESS"`, `"ERROR"` or `"UNKNOWN"` (if for example the operation was cancelled during execution). +|=== + + +[[observability.jms]] +== JMS messaging instrumentation + +Spring Framework uses the Jakarta JMS instrumentation provided by Micrometer if the `io.micrometer:micrometer-jakarta9` dependency is on the classpath. +The `io.micrometer.jakarta9.instrument.jms.JmsInstrumentation` instruments `jakarta.jms.Session` and records the relevant observations. + +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. +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`: + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`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"`). +|=== + +.High cardinality Keys +[cols="a,a"] +|=== +|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.message.id` |Value used by the messaging system as an identifier for the message. +|=== + +[[observability.jms.publish]] +=== JMS message Publication instrumentation + +`"jms.message.publish"` observations are recorded when a JMS message is sent to the broker. +They measure the time spent sending the message and propagate the tracing information with outgoing JMS message headers. + +You will need to configure the `ObservationRegistry` on the `JmsTemplate` to enable observations: + +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 + +`"jms.message.process"` observations are recorded when a JMS message is processed by the application. +They measure the time spent processing the message and propagate the tracing context with incoming JMS message headers. + +Most applications will use the xref:integration/jms/annotated.adoc#jms-annotated[`@JmsListener` annotated methods] mechanism to process incoming messages. +You will need to ensure that the `ObservationRegistry` is configured on the dedicated `JmsListenerContainerFactory`: + +include-code::./JmsConfiguration[] + +A xref:integration/jms/annotated.adoc#jms-annotated-support[default container factory is required to enable the annotation support], +but note that `@JmsListener` annotations can refer to specific container factory beans for specific purposes. +In all cases, Observations are only recorded if the observation registry is configured on the container factory. + +Similar observations are recorded with `JmsTemplate` when messages are processed by a `MessageListener`. +Such listeners are set on a `MessageConsumer` within a session callback (see `JmsTemplate.execute(SessionCallback)`). + +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 @@ -99,7 +203,7 @@ include-code::./UserController[] NOTE: Because the instrumentation is done at the Servlet Filter level, the observation scope only covers the filters ordered after this one as well as the handling of the request. 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 of the scope of this project. +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. By default, the following `KeyValues` are created: @@ -107,7 +211,8 @@ By default, the following `KeyValues` are created: [cols="a,a"] |=== |Name | Description -|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE`} if no exception happened. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |`method` _(required)_|Name of 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. @@ -125,11 +230,15 @@ By default, the following `KeyValues` are created: [[observability.http-server.reactive]] === Reactive applications -Applications need to configure the `org.springframework.web.filter.reactive.ServerHttpObservationFilter` reactive `WebFilter` in their application. -It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`. +Applications need to configure the `WebHttpHandlerBuilder` with a `MeterRegistry` to enable server instrumentation. +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`. -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 `WebFilter`. -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. +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. You can, at any point during request processing, set the error field on the `ObservationContext` yourself: include-code::./UserController[] @@ -140,7 +249,8 @@ By default, the following `KeyValues` are created: [cols="a,a"] |=== |Name | Description -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`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. |`outcome` _(required)_|Outcome of the HTTP server exchange. |`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created. @@ -179,7 +289,8 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ |`client.name` _(required)_|Client name derived from the request URI host. |`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received. |`outcome` _(required)_|Outcome of the HTTP client exchange. -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |=== .High cardinality Keys @@ -190,6 +301,33 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ |=== +[[observability.http-client.restclient]] +=== RestClient + +Applications must configure an `ObservationRegistry` on the `RestClient.Builder` to enable the instrumentation; without that, observations are "no-ops". + +Instrumentation uses the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. + +.Low cardinality Keys +[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. +|`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. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. +|=== + +.High cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`http.url` _(required)_|HTTP request URI. +|=== + [[observability.http-client.webclient]] === WebClient @@ -208,7 +346,8 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien |`client.name` _(required)_|Client name derived from the request URI host. |`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received. |`outcome` _(required)_|Outcome of the HTTP client exchange. -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |=== .High cardinality Keys @@ -219,3 +358,28 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien |=== +[[observability.application-events]] +== Application Events and `@EventListener` + +Spring Framework does not contribute Observations for xref:core/beans/context-introduction.adoc#context-functionality-events-annotation[`@EventListener` calls], +as they don't have the right semantics for such instrumentation. +By default, event publication and processing are done synchronously and on the same thread. +This means that during the execution of that task, the ThreadLocals and logging context will be the same as the event publisher. + +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 {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: + +include-code::./ApplicationEventsConfiguration[] + +Similarly, if that asynchronous choice is made locally for each `@EventListener` annotated method, by adding `@Async` to it, +you can choose a `TaskExecutor` that propagates context by referring to it by its qualifier. +Given the following `TaskExecutor` bean definition, configured with the dedicated task decorator: + +include-code::./EventAsyncExecutionConfiguration[] + +Annotating event listeners with `@Async` and the relevant qualifier will achieve similar context propagation results: + +include-code::./EmailNotificationListener[] diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index 5bd57bdfb556..0e0c5d2de63b 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -3,361 +3,912 @@ 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. -[[rest-webclient]] -== `WebClient` +[[rest-restclient]] +== `RestClient` -`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was -introduced in 5.0 and offers an alternative to the `RestTemplate`, with support for -synchronous, asynchronous, and streaming scenarios. +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. -`WebClient` supports the following: +=== Creating a `RestClient` -* Non-blocking I/O. -* Reactive Streams back pressure. -* High concurrency with fewer hardware resources. -* Functional-style, fluent API that takes advantage of Java 8 lambdas. -* Synchronous and asynchronous interactions. -* Streaming up to or streaming down from a server. +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. -See xref:web/webflux-webclient.adoc[WebClient] for more details. +Once created (or built), the `RestClient` can be used safely by multiple threads. +The following sample shows how to create a default `RestClient`, and how to build a custom one. +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim",role="primary"] +---- +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(); +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +---- +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() +---- +====== -[[rest-resttemplate]] -== `RestTemplate` +=== Using the `RestClient` -The `RestTemplate` provides a higher level API over HTTP client libraries. It makes it -easy to invoke REST endpoints in a single line. It exposes the following groups of -overloaded methods: +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. -NOTE: `RestTemplate` is in maintenance mode, with only requests for minor -changes and bugs to be accepted. Please, consider using the -xref:web/webflux-webclient.adoc[WebClient] instead. +==== Request URL -[[rest-overview-of-resttemplate-methods-tbl]] -.RestTemplate methods -[cols="1,3"] -|=== -| Method group | Description +Next, the request URI can be specified with the `uri` methods. +This step is optional and can be skipped if the `RestClient` is configured with a default URI. +The URL is typically specified as a `String`, with optional URI template variables. +The following example configures a GET request to `https://example.com/orders/42`: -| `getForObject` -| Retrieves a representation via GET. +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +int id = 42; +restClient.get() + .uri("https://example.com/orders/{id}", id) + .... +---- -| `getForEntity` -| Retrieves a `ResponseEntity` (that is, status, headers, and body) by using GET. +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +val id = 42 +restClient.get() + .uri("https://example.com/orders/{id}", id) + ... +---- +====== -| `headForHeaders` -| Retrieves all headers for a resource by using HEAD. +A function can also be used for more controls, such as specifying xref:web/webmvc/mvc-uri-building.adoc[request parameters]. -| `postForLocation` -| Creates a new resource by using POST and returns the `Location` header from the response. +String URLs are encoded by default, but this can be changed by building a client with a custom `uriBuilderFactory`. +The URL can also be provided with a function or as a `java.net.URI`, both of which are not encoded. +For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-building.adoc[URI Links]. -| `postForObject` -| Creates a new resource by using POST and returns the representation from the response. +==== Request headers and body -| `postForEntity` -| Creates a new resource by using POST and returns the representation from the response. +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)`. -| `put` -| Creates or updates a resource by using PUT. +The request body itself can be set by `body(Object)`, which internally uses <>. +Alternatively, the request body can be set using a `ParameterizedTypeReference`, allowing you to use generics. +Finally, the body can be set to a callback function that writes to an `OutputStream`. -| `patchForObject` -| Updates a resource by using PATCH and returns the representation from the response. -Note that the JDK `HttpURLConnection` does not support `PATCH`, but Apache -HttpComponents and others do. +==== Retrieving the response -| `delete` -| Deletes the resources at the specified URI by using DELETE. +Once the request has been set up, the HTTP response is accessed by invoking `retrieve()`. +The response body can be accessed by using `body(Class)` or `body(ParameterizedTypeReference)` for parameterized types like lists. +The `body` method converts the response contents into various types – for instance, bytes can be converted into a `String`, JSON can be converted into objects using Jackson, and so on (see <>). -| `optionsForAllow` -| Retrieves allowed HTTP methods for a resource by using ALLOW. +The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body. -| `exchange` -| More generalized (and less opinionated) version of the preceding methods that provides extra -flexibility when needed. It accepts a `RequestEntity` (including HTTP method, URL, headers, -and body as input) and returns a `ResponseEntity`. +This sample shows how `RestClient` can be used to perform a simple `GET` request. -These methods allow the use of `ParameterizedTypeReference` instead of `Class` to specify -a response type with generics. +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +String result = restClient.get() <1> + .uri("https://example.com") <2> + .retrieve() <3> + .body(String.class); <4> -| `execute` -| The most generalized way to perform a request, with full control over request -preparation and response extraction through callback interfaces. +System.out.println(result); <5> +---- +<1> Set up a GET request +<2> Specify the URL to connect to +<3> Retrieve the response +<4> Convert the response into a string +<5> Print the result + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +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 +<3> Retrieve the response +<4> Convert the response into a string +<5> Print the result +====== + +Access to the response status code and headers is provided through `ResponseEntity`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +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` +<3> Print the result -[[rest-resttemplate-create]] -=== Initialization +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +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` +<3> Print the result +====== + +`RestClient` can convert JSON to objects, using the Jackson library. +Note the usage of URI variables in this sample and that the `Accept` header is set to JSON. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +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` +<3> Convert the JSON response into a `Pet` domain object -The default constructor uses `java.net.HttpURLConnection` to perform requests. You can -switch to a different HTTP library with an implementation of `ClientHttpRequestFactory`. -Currently, there is also built-in support for Apache HttpComponents and OkHttp. +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +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` +<3> Convert the JSON response into a `Pet` domain object +====== + +In the next sample, `RestClient` is used to perform a POST request that contains JSON, which again is converted using Jackson. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +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 +<3> Set the `Content-Type` header to `application/json` +<4> Use `pet` as the request body +<5> Convert the response into a response entity with no body. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +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 +<3> Set the `Content-Type` header to `application/json` +<4> Use `pet` as the request body +<5> Convert the response into a response entity with no body. +====== + +==== Error handling + +By default, `RestClient` throws a subclass of `RestClientException` when retrieving a response with a 4xx or 5xx status code. +This behavior can be overridden using `onStatus`. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +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 +<3> Throw a custom exception + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +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 +<3> Throw a custom exception +====== -For example, to switch to Apache HttpComponents, you can use the following: +==== Exchange -[source,java,indent=0,subs="verbatim,quotes"] +For more advanced scenarios, the `RestClient` gives access to the underlying HTTP request and response through the `exchange()` method, which can be used instead of `retrieve()`. +Status handlers are not applied when use `exchange()`, because the exchange function already provides access to the full response, allowing you to perform any error handling necessary. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); +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 +<3> Convert the response into a Pet domain object -Each `ClientHttpRequestFactory` exposes configuration options specific to the underlying -HTTP client library -- for example, for credentials, connection pooling, and other details. +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 + } + } +---- +<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 +====== -TIP: Note that the `java.net` implementation for HTTP requests can raise an exception when -accessing the status of a response that represents an error (such as 401). If this is an -issue, switch to another HTTP client library. -NOTE: `RestTemplate` can be instrumented for observability, in order to produce metrics and traces. -See the xref:integration/observability.adoc#http-client.resttemplate[RestTemplate Observability support] section. +[[rest-message-conversion]] +=== HTTP Message Conversion -[[rest-resttemplate-uri]] -==== URIs +[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs[See equivalent in the Reactive stack]# -Many of the `RestTemplate` methods accept a URI template and URI template variables, -either as a `String` variable argument, or as `Map`. +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). -The following example uses a `String` variable argument: +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]). -[source,java,indent=0,subs="verbatim,quotes"] ----- - String result = restTemplate.getForObject( - "https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21"); ----- +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. -The following example uses a `Map`: +[[rest-message-converters-tbl]] +.HttpMessageConverter Implementations +[cols="1,3"] +|=== +| MessageConverter | Description -[source,java,indent=0,subs="verbatim,quotes"] ----- - Map vars = Collections.singletonMap("hotel", "42"); +| `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`. - String result = restTemplate.getForObject( - "https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars); ----- +| `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[])`. -Keep in mind URI templates are automatically encoded, as the following example shows: +| `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`. -[source,java,indent=0,subs="verbatim,quotes"] ----- - restTemplate.getForObject("https://example.com/hotel list", String.class); +| `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`. - // Results in request to "https://example.com/hotel%20list" +==== Jackson JSON Views + +To serialize only a subset of the object properties, you can specify a {baeldung-blog}/jackson-json-view-annotation[Jackson JSON View], as the following example shows: + +[source,java,indent=0,subs="verbatim"] ---- +MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); +value.setSerializationView(User.WithoutPasswordView.class); -You can use the `uriTemplateHandler` property of `RestTemplate` to customize how URIs -are encoded. Alternatively, you can prepare a `java.net.URI` and pass it into one of -the `RestTemplate` methods that accepts a `URI`. +ResponseEntity response = restClient.post() // or RestTemplate.postForEntity + .contentType(APPLICATION_JSON) + .body(value) + .retrieve() + .toBodilessEntity(); -For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-building.adoc[URI Links]. +---- -[[rest-template-headers]] -==== Headers +==== Multipart -You can use the `exchange()` methods to specify request headers, as the following example shows: +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. +For example: -[source,java,indent=0,subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim"] ---- - String uriTemplate = "https://example.com/hotels/{hotel}"; - URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); +MultiValueMap parts = new LinkedMultiValueMap<>(); - RequestEntity requestEntity = RequestEntity.get(uri) - .header("MyRequestHeader", "MyValue") - .build(); +parts.add("fieldPart", "fieldValue"); +parts.add("filePart", new FileSystemResource("...logo.png")); +parts.add("jsonPart", new Person("Jason")); - ResponseEntity response = template.exchange(requestEntity, String.class); +HttpHeaders headers = new HttpHeaders(); +headers.setContentType(MediaType.APPLICATION_XML); +parts.add("xmlPart", new HttpEntity<>(myBean, headers)); - String responseHeader = response.getHeaders().getFirst("MyResponseHeader"); - String body = response.getBody(); +// send using RestClient.post or RestTemplate.postForEntity ---- -You can obtain response headers through many `RestTemplate` method variants that return -`ResponseEntity`. +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. -[[rest-template-body]] -=== Body +Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`). -Objects passed into and returned from `RestTemplate` methods are converted to and from raw -content with the help of an `HttpMessageConverter`. +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. -On a POST, an input object is serialized to the request body, as the following example shows: +[[rest-request-factories]] +=== Client Request Factories ----- -URI location = template.postForLocation("https://example.com/people", person); ----- +To execute the HTTP request, `RestClient` uses a client HTTP library. +These libraries are adapted via the `ClientRequestFactory` interface. +Various implementations are available: -You need not explicitly set the Content-Type header of the request. In most cases, -you can find a compatible message converter based on the source `Object` type, and the chosen -message converter sets the content type accordingly. If necessary, you can use the -`exchange` methods to explicitly provide the `Content-Type` request header, and that, in -turn, influences what message converter is selected. +* `JdkClientHttpRequestFactory` for Java's `HttpClient` +* `HttpComponentsClientHttpRequestFactory` for use with Apache HTTP Components `HttpClient` +* `JettyClientHttpRequestFactory` for Jetty's `HttpClient` +* `ReactorNettyClientRequestFactory` for Reactor Netty's `HttpClient` +* `SimpleClientHttpRequestFactory` as a simple default -On a GET, the body of the response is deserialized to an output `Object`, as the following example shows: ----- -Person person = restTemplate.getForObject("https://example.com/people/{id}", Person.class, 42); ----- +If no request factory is specified when the `RestClient` was built, it will use the Apache or Jetty `HttpClient` if they are available on the classpath. +Otherwise, if the `java.net.http` module is loaded, it will use Java's `HttpClient`. +Finally, it will resort to the simple default. -The `Accept` header of the request does not need to be explicitly set. In most cases, -a compatible message converter can be found based on the expected response type, which -then helps to populate the `Accept` header. If necessary, you can use the `exchange` -methods to provide the `Accept` header explicitly. +TIP: Note that the `SimpleClientHttpRequestFactory` may raise an exception when accessing the status of a response that represents an error (e.g. 401). +If this is an issue, use any of the alternative request factories. -By default, `RestTemplate` registers all built-in -xref:integration/rest-clients.adoc#rest-message-conversion[message converters], depending on classpath checks that help -to determine what optional conversion libraries are present. You can also set the message -converters to use explicitly. +[[rest-webclient]] +== `WebClient` -[[rest-message-conversion]] -==== Message Conversion -[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs[See equivalent in the Reactive stack]# +`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was +introduced in 5.0 and offers an alternative to the `RestTemplate`, with support for +synchronous, asynchronous, and streaming scenarios. -The `spring-web` module contains the `HttpMessageConverter` contract 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 `RestTemplate`) and -on the server side (for example, in Spring MVC REST controllers). +`WebClient` supports the following: -Concrete implementations for the main media (MIME) types are provided in the framework -and are, by default, registered with the `RestTemplate` on the client side and with -`RequestMappingHandlerAdapter` on the server side (see -xref:web/webmvc/mvc-config/message-converters.adoc[Configuring Message Converters]). +* Non-blocking I/O +* Reactive Streams back pressure +* High concurrency with fewer hardware resources +* Functional-style, fluent API that takes advantage of Java 8 lambdas +* Synchronous and asynchronous interactions +* Streaming up to or streaming down from a server -The implementations of `HttpMessageConverter` are described in the following sections. -For all converters, a default media type is used, but you can override it by setting the -`supportedMediaTypes` bean property. The following table describes each implementation: +See xref:web/webflux-webclient.adoc[WebClient] for more details. -[[rest-message-converters-tbl]] -.HttpMessageConverter Implementations + + + +[[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. +For asynchronous and streaming scenarios, consider the reactive xref:web/webflux-webclient.adoc[WebClient]. + +[[rest-overview-of-resttemplate-methods-tbl]] +.RestTemplate methods [cols="1,3"] |=== -| MessageConverter | Description +| Method group | 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`. +| `getForObject` +| Retrieves a representation via GET. -| `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. As of Spring Framework 5.2, additional multipart subtypes can be supported for -writing form data. Consult the javadoc for `FormHttpMessageConverter` for further details. +| `getForEntity` +| Retrieves a `ResponseEntity` (that is, status, headers, and body) by using GET. -| `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[])`. +| `headForHeaders` +| Retrieves all headers for a resource by using HEAD. -| `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`. +| `postForLocation` +| Creates a new resource by using POST and returns the `Location` header from the response. -| `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`. +| `postForObject` +| Creates a new resource by using POST and returns the representation from the response. -| `MappingJackson2XmlHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write XML by using -https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's -`XmlMapper`. You can customize XML mapping as needed through the use of JAXB -or Jackson's provided annotations. When you need further control (for cases where custom XML -serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` -through the `ObjectMapper` property. By default, this -converter supports `application/xml`. +| `postForEntity` +| Creates a new resource by using POST and returns the representation from the response. -| `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`. +| `put` +| Creates or updates a resource by using PUT. -| `BufferedImageHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write -`java.awt.image.BufferedImage` from the HTTP request and response. This converter reads -and writes the media type supported by the Java I/O API. +| `patchForObject` +| Updates a resource by using PATCH and returns the representation from the response. +Note that the JDK `HttpURLConnection` does not support `PATCH`, but Apache HttpComponents and others do. -|=== +| `delete` +| Deletes the resources at the specified URI by using DELETE. -[[rest-template-jsonview]] -=== Jackson JSON Views +| `optionsForAllow` +| Retrieves allowed HTTP methods for a resource by using ALLOW. -You can specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON View] -to serialize only a subset of the object properties, as the following example shows: +| `exchange` +| More generalized (and less opinionated) version of the preceding methods that provides extra flexibility when needed. +It accepts a `RequestEntity` (including HTTP method, URL, headers, and body as input) and returns a `ResponseEntity`. -[source,java,indent=0,subs="verbatim,quotes"] ----- - MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); - value.setSerializationView(User.WithoutPasswordView.class); +These methods allow the use of `ParameterizedTypeReference` instead of `Class` to specify +a response type with generics. - RequestEntity requestEntity = - RequestEntity.post(new URI("https://example.com/user")).body(value); +| `execute` +| The most generalized way to perform a request, with full control over request +preparation and response extraction through callback interfaces. - ResponseEntity response = template.exchange(requestEntity, String.class); ----- +|=== -[[rest-template-multipart]] -=== Multipart +=== Initialization -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. For example: +`RestTemplate` uses the same HTTP library abstraction as `RestClient`. +By default, it uses the `SimpleClientHttpRequestFactory`, but this can be changed via the constructor. +See <>. -[source,java,indent=0,subs="verbatim,quotes"] ----- - MultiValueMap parts = new LinkedMultiValueMap<>(); +NOTE: `RestTemplate` can be instrumented for observability, in order to produce metrics and traces. +See the xref:integration/observability.adoc#http-client.resttemplate[RestTemplate Observability support] section. - parts.add("fieldPart", "fieldValue"); - parts.add("filePart", new FileSystemResource("...logo.png")); - parts.add("jsonPart", new Person("Jason")); +[[rest-template-body]] +=== Body - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_XML); - parts.add("xmlPart", new HttpEntity<>(myBean, headers)); ----- +Objects passed into and returned from `RestTemplate` methods are converted to and from HTTP messages with the help of an `HttpMessageConverter`, see <>. -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. +=== Migrating from `RestTemplate` to `RestClient` -Once the `MultiValueMap` is ready, you can pass it to the `RestTemplate`, as show below: +The following table shows `RestClient` equivalents for `RestTemplate` methods. +It can be used to migrate from the latter to the former. -[source,java,indent=0,subs="verbatim,quotes"] ----- - MultiValueMap parts = ...; - template.postForObject("https://example.com/upload", parts, Void.class); ----- +.RestClient equivalents for RestTemplate methods +[cols="1,1", options="header"] +|=== +| `RestTemplate` method | `RestClient` equivalent + +| `getForObject(String, Class, Object...)` +| `get() +.uri(String, Object...) +.retrieve() +.body(Class)` + +| `getForObject(String, Class, Map)` +| `get() +.uri(String, Map) +.retrieve() +.body(Class)` + +| `getForObject(URI, Class)` +| `get() +.uri(URI) +.retrieve() +.body(Class)` + + +| `getForEntity(String, Class, Object...)` +| `get() +.uri(String, Object...) +.retrieve() +.toEntity(Class)` + +| `getForEntity(String, Class, Map)` +| `get() +.uri(String, Map) +.retrieve() +.toEntity(Class)` + +| `getForEntity(URI, Class)` +| `get() +.uri(URI) +.retrieve() +.toEntity(Class)` + + +| `headForHeaders(String, Object...)` +| `head() +.uri(String, Object...) +.retrieve() +.toBodilessEntity() +.getHeaders()` + +| `headForHeaders(String, Map)` +| `head() +.uri(String, Map) +.retrieve() +.toBodilessEntity() +.getHeaders()` + +| `headForHeaders(URI)` +| `head() +.uri(URI) +.retrieve() +.toBodilessEntity() +.getHeaders()` + + +| `postForLocation(String, Object, Object...)` +| `post() +.uri(String, Object...) +.body(Object).retrieve() +.toBodilessEntity() +.getLocation()` + +| `postForLocation(String, Object, Map)` +| `post() +.uri(String, Map) +.body(Object) +.retrieve() +.toBodilessEntity() +.getLocation()` + +| `postForLocation(URI, Object)` +| `post() +.uri(URI) +.body(Object) +.retrieve() +.toBodilessEntity() +.getLocation()` + + +| `postForObject(String, Object, Class, Object...)` +| `post() +.uri(String, Object...) +.body(Object) +.retrieve() +.body(Class)` + +| `postForObject(String, Object, Class, Map)` +| `post() +.uri(String, Map) +.body(Object) +.retrieve() +.body(Class)` + +| `postForObject(URI, Object, Class)` +| `post() +.uri(URI) +.body(Object) +.retrieve() +.body(Class)` + + +| `postForEntity(String, Object, Class, Object...)` +| `post() +.uri(String, Object...) +.body(Object) +.retrieve() +.toEntity(Class)` + +| `postForEntity(String, Object, Class, Map)` +| `post() +.uri(String, Map) +.body(Object) +.retrieve() +.toEntity(Class)` + +| `postForEntity(URI, Object, Class)` +| `post() +.uri(URI) +.body(Object) +.retrieve() +.toEntity(Class)` + + +| `put(String, Object, Object...)` +| `put() +.uri(String, Object...) +.body(Object) +.retrieve() +.toBodilessEntity()` + +| `put(String, Object, Map)` +| `put() +.uri(String, Map) +.body(Object) +.retrieve() +.toBodilessEntity()` + +| `put(URI, Object)` +| `put() +.uri(URI) +.body(Object) +.retrieve() +.toBodilessEntity()` + + +| `patchForObject(String, Object, Class, Object...)` +| `patch() +.uri(String, Object...) +.body(Object) +.retrieve() +.body(Class)` + +| `patchForObject(String, Object, Class, Map)` +| `patch() +.uri(String, Map) +.body(Object) +.retrieve() +.body(Class)` + +| `patchForObject(URI, Object, Class)` +| `patch() +.uri(URI) +.body(Object) +.retrieve() +.body(Class)` + + +| `delete(String, Object...)` +| `delete() +.uri(String, Object...) +.retrieve() +.toBodilessEntity()` + +| `delete(String, Map)` +| `delete() +.uri(String, Map) +.retrieve() +.toBodilessEntity()` + +| `delete(URI)` +| `delete() +.uri(URI) +.retrieve() +.toBodilessEntity()` + + +| `optionsForAllow(String, Object...)` +| `options() +.uri(String, Object...) +.retrieve() +.toBodilessEntity() +.getAllow()` + +| `optionsForAllow(String, Map)` +| `options() +.uri(String, Map) +.retrieve() +.toBodilessEntity() +.getAllow()` + +| `optionsForAllow(URI)` +| `options() +.uri(URI) +.retrieve() +.toBodilessEntity() +.getAllow()` + + +| `exchange(String, HttpMethod, HttpEntity, Class, Object...)` +| `method(HttpMethod) +.uri(String, Object...) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(Class)` footnote:http-entity[`HttpEntity` headers and body have to be supplied to the `RestClient` via `headers(Consumer)` and `body(Object)`.] + +| `exchange(String, HttpMethod, HttpEntity, Class, Map)` +| `method(HttpMethod) +.uri(String, Map) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(Class)` footnote:http-entity[] + +| `exchange(URI, HttpMethod, HttpEntity, Class)` +| `method(HttpMethod) +.uri(URI) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(Class)` footnote:http-entity[] + + +| `exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Object...)` +| `method(HttpMethod) +.uri(String, Object...) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(ParameterizedTypeReference)` footnote:http-entity[] + +| `exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Map)` +| `method(HttpMethod) +.uri(String, Map) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(ParameterizedTypeReference)` footnote:http-entity[] + +| `exchange(URI, HttpMethod, HttpEntity, ParameterizedTypeReference)` +| `method(HttpMethod) +.uri(URI) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(ParameterizedTypeReference)` footnote:http-entity[] + + +| `exchange(RequestEntity, Class)` +| `method(HttpMethod) +.uri(URI) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(Class)` footnote:request-entity[`RequestEntity` method, URI, headers and body have to be supplied to the `RestClient` via `method(HttpMethod)`, `uri(URI)`, `headers(Consumer)` and `body(Object)`.] + +| `exchange(RequestEntity, ParameterizedTypeReference)` +| `method(HttpMethod) +.uri(URI) +.headers(Consumer) +.body(Object) +.retrieve() +.toEntity(ParameterizedTypeReference)` footnote:request-entity[] + + +| `execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object...)` +| `method(HttpMethod) +.uri(String, Object...) +.exchange(ExchangeFunction)` + +| `execute(String, HttpMethod, RequestCallback, ResponseExtractor, Map)` +| `method(HttpMethod) +.uri(String, Map) +.exchange(ExchangeFunction)` + +| `execute(URI, HttpMethod, RequestCallback, ResponseExtractor)` +| `method(HttpMethod) +.uri(URI) +.exchange(ExchangeFunction)` -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` is defaulted to `application/x-www-form-urlencoded`. -If necessary the `Content-Type` may also be set explicitly. +|=== [[rest-http-interface]] == HTTP Interface -The Spring Framework lets you define an HTTP service as a Java interface with annotated -methods for HTTP exchanges. You can then generate a proxy that implements this interface -and performs the exchanges. This helps to simplify HTTP remote access which often -involves a facade that wraps the details of using the underlying HTTP client. +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. -One, declare an interface with `@HttpExchange` methods: +Start by creating the interface with `@HttpExchange` methods: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -371,12 +922,38 @@ One, declare an interface with `@HttpExchange` methods: } ---- -Two, create a proxy that will perform the declared HTTP exchanges: +Now you can create a proxy that performs requests when methods are called. + +For `RestClient`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build(); + RestClientAdapter adapter = RestClientAdapter.create(restClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); + + RepositoryService service = factory.createClient(RepositoryService.class); +---- + +For `WebClient`: [source,java,indent=0,subs="verbatim,quotes"] ---- - WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build(); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build(); + WebClient webClient = WebClient.builder().baseUrl("https://api.github.com/").build(); + WebClientAdapter adapter = WebClientAdapter.create(webClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); + + RepositoryService service = factory.createClient(RepositoryService.class); +---- + +For `RestTemplate`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/")); + RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); RepositoryService service = factory.createClient(RepositoryService.class); ---- @@ -412,6 +989,10 @@ method parameters: | `URI` | Dynamically set the URL for the request, overriding the annotation's `url` attribute. +| `UriBuilderFactory` +| Provide a `UriBuilderFactory` to expand the URI template and URI variables with. + In effect, replaces the `UriBuilderFactory` (and its base URL) of the underlying client. + | `HttpMethod` | Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute @@ -425,6 +1006,9 @@ method parameters: `Map` with multiple variables, or an individual value. Type conversion is supported for non-String values. +| `@RequestAttribute` +| Provide an `Object` to add as a request attribute. Only supported by `WebClient`. + | `@RequestBody` | Provide the body of the request either as an Object to be serialized, or a Reactive Streams `Publisher` such as `Mono`, `Flux`, or any other async type supported @@ -444,6 +1028,10 @@ method parameters: Object (entity to be encoded, e.g. as JSON), `HttpEntity` (part content and headers), a Spring `Part`, or Reactive Streams `Publisher` of any of the above. +| `MultipartFile` +| Add a request part from a `MultipartFile`, typically used in a Spring MVC controller + where it represents an uploaded file. + | `@CookieValue` | Add a cookie or multiple cookies. The argument may be a `Map` or `MultiValueMap` with multiple cookies, a `Collection` of values, or an @@ -455,51 +1043,105 @@ method parameters: [[rest-http-interface-return-values]] === Return Values -Annotated, HTTP exchange methods support the following return values: +The supported return values depend on the underlying client. + +Clients adapted to `HttpExchangeAdapter` such as `RestClient` and `RestTemplate` +support synchronous return values: + +[cols="1,2", options="header"] +|=== +| Method return value | Description + +| `void` +| Perform the given request. + +| `HttpHeaders` +| Perform the given request and return the response headers. + +| `` +| Perform the given request and decode the response content to the declared return type. + +| `ResponseEntity` +| Perform the given request and return a `ResponseEntity` with the status and headers. + +| `ResponseEntity` +| Perform the given request, decode the response content to the declared return type, and + return a `ResponseEntity` with the status, headers, and the decoded body. + +|=== + +Clients adapted to `ReactorHttpExchangeAdapter` such as `WebClient`, support all of above +as well as reactive variants. The table below shows Reactor types, but you can also use +other reactive types that are supported through the `ReactiveAdapterRegistry`: [cols="1,2", options="header"] |=== | Method return value | Description -| `void`, `Mono` +| `Mono` | Perform the given request, and release the response content, if any. -| `HttpHeaders`, `Mono` +| `Mono` | Perform the given request, release the response content, if any, and return the - response headers. +response headers. -| ``, `Mono` +| `Mono` | Perform the given request and decode the response content to the declared return type. -| ``, `Flux` +| `Flux` | Perform the given request and decode the response content to a stream of the declared - element type. +element type. -| `ResponseEntity`, `Mono>` +| `Mono>` | Perform the given request, and release the response content, if any, and return a - `ResponseEntity` with the status and headers. +`ResponseEntity` with the status and headers. -| `ResponseEntity`, `Mono>` +| `Mono>` | Perform the given request, decode the response content to the declared return type, and - return a `ResponseEntity` with the status, headers, and the decoded body. +return a `ResponseEntity` with the status, headers, and the decoded body. | `Mono>` | Perform the given request, decode the response content to a stream of the declared - element type, and return a `ResponseEntity` with the status, headers, and the decoded - response body stream. +element type, and return a `ResponseEntity` with the status, headers, and the decoded +response body stream. |=== -TIP: You can also use any other async or reactive types registered in the -`ReactiveAdapterRegistry`. +By default, the timeout for synchronous return values with `ReactorHttpExchangeAdapter` +depends on how the underlying HTTP client is configured. You can set a `blockTimeout` +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. [[rest-http-interface-exceptions]] -=== Exception Handling +=== Error Handling + +To customize error response handling, you need to configure the underlying HTTP client. + +For `RestClient`: -By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status -codes. To customize this, you can register a response status handler that applies to all -responses performed through the client: +By default, `RestClient` raises `RestClientException` for 4xx and 5xx HTTP status codes. +To customize this, register a response status handler that applies to all responses +performed through the client: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestClient restClient = RestClient.builder() + .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...) + .build(); + + RestClientAdapter adapter = RestClientAdapter.create(restClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); +---- + +For more details and options, such as suppressing error status codes, see the Javadoc of +`defaultStatusHandler` in `RestClient.Builder`. + +For `WebClient`: + +By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status codes. +To customize this, register a response status handler that applies to all responses +performed through the client: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -507,10 +1149,28 @@ responses performed through the client: .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) .build(); - WebClientAdapter clientAdapter = WebClientAdapter.forClient(webClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory - .builder(clientAdapter).build(); + WebClientAdapter adapter = WebClientAdapter.create(webClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(adapter).build(); ---- For more details and options, such as suppressing error status codes, see the Javadoc of `defaultStatusHandler` in `WebClient.Builder`. + +For `RestTemplate`: + +By default, `RestTemplate` raises `RestClientException` for 4xx and 5xx HTTP status codes. +To customize this, register an error handler that applies to all responses +performed through the client: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(myErrorHandler); + + RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); +---- + +For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and +the `ResponseErrorHandler` hierarchy. + diff --git a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc index 889209ebe972..796d1ea326d4 100644 --- a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc +++ b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc @@ -66,6 +66,11 @@ The variants that Spring provides are as follows: 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]] === Using a `TaskExecutor` @@ -244,6 +249,13 @@ to provide common bean-style configuration along the lines of `ThreadPoolTaskExe These variants work perfectly fine for locally embedded thread pool setups in lenient application server environments, as well -- in particular on Tomcat and Jetty. +As of 6.1, `ThreadPoolTaskScheduler` provides a pause/resume capability and graceful +shutdown through Spring's lifecycle management. There is also a new option called +`SimpleAsyncTaskScheduler` which is aligned with JDK 21's Virtual Threads, using a +single scheduler thread but firing up a new thread for every scheduled task execution +(except for fixed-delay tasks which all operate on a single scheduler thread, so for +this virtual-thread-aligned option, fixed rates and cron triggers are recommended). + [[scheduling-annotation-support]] @@ -272,8 +284,8 @@ You can pick and choose the relevant annotations for your application. For examp if you need only support for `@Scheduled`, you can omit `@EnableAsync`. For more fine-grained control, you can additionally implement the `SchedulingConfigurer` interface, the `AsyncConfigurer` interface, or both. See the -{api-spring-framework}/scheduling/annotation/SchedulingConfigurer.html[`SchedulingConfigurer`] -and {api-spring-framework}/scheduling/annotation/AsyncConfigurer.html[`AsyncConfigurer`] +{spring-framework-api}/scheduling/annotation/SchedulingConfigurer.html[`SchedulingConfigurer`] +and {spring-framework-api}/scheduling/annotation/AsyncConfigurer.html[`AsyncConfigurer`] javadoc for full details. If you prefer XML configuration, you can use the `` element, @@ -353,6 +365,17 @@ the amount of time to wait before the first execution of the method, as the foll } ---- +For one-time tasks, you can just specify an initial delay by indicating the amount +of time to wait before the intended execution of the method: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(initialDelay = 1000) + public void doSomething() { + // something that should run only once + } +---- + If simple periodic scheduling is not expressive enough, you can provide a xref:integration/scheduling.adoc#scheduling-cron-expression[cron expression]. The following example runs only on weekdays: @@ -392,6 +415,120 @@ container and once through the `@Configurable` aspect), with the consequence of `@Scheduled` method being invoked twice. ==== +[[scheduling-annotation-support-scheduled-reactive]] +=== The `@Scheduled` annotation on Reactive methods or Kotlin suspending functions + +As of Spring Framework 6.1, `@Scheduled` methods are also supported on several types +of reactive methods: + + - methods with a `Publisher` return type (or any concrete implementation of `Publisher`) +like in the following example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(fixedDelay = 500) + public Publisher reactiveSomething() { + // return an instance of Publisher + } +---- + + - methods with a return type that can be adapted to `Publisher` via the shared instance +of the `ReactiveAdapterRegistry`, provided the type supports _deferred subscription_ like +in the following example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(fixedDelay = 500) + public Single rxjavaNonPublisher() { + return Single.just("example"); + } +---- + +[NOTE] +==== +The `CompletableFuture` class is an example of a type that can typically be adapted +to `Publisher` but doesn't support deferred subscription. Its `ReactiveAdapter` in the +registry denotes that by having the `getDescriptor().isDeferred()` method return `false`. +==== + + - Kotlin suspending functions, like in the following example: + +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(fixedDelay = 500) + suspend fun something() { + // do something asynchronous + } +---- + + - methods that return a Kotlin `Flow` or `Deferred` instance, like in the following example: + +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(fixedDelay = 500) + fun something(): Flow { + flow { + // do something asynchronous + } + } +---- + +All these types of methods must be declared without any arguments. In the case of Kotlin +suspending functions, the `kotlinx.coroutines.reactor` bridge must also be present to allow +the framework to invoke a suspending function as a `Publisher`. + +The Spring Framework will obtain a `Publisher` for the annotated method once and will +schedule a `Runnable` in which it subscribes to said `Publisher`. These inner regular +subscriptions occur according to the corresponding `cron`/`fixedDelay`/`fixedRate` configuration. + +If the `Publisher` emits `onNext` signal(s), these are ignored and discarded (the same way +return values from synchronous `@Scheduled` methods are ignored). + +In the following example, the `Flux` emits `onNext("Hello")`, `onNext("World")` every 5 +seconds, but these values are unused: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(initialDelay = 5000, fixedRate = 5000) + public Flux reactiveSomething() { + return Flux.just("Hello", "World"); + } +---- + +If the `Publisher` emits an `onError` signal, it is logged at `WARN` level and recovered. +Because of the asynchronous and lazy nature of `Publisher` instances, exceptions are +not thrown from the `Runnable` task: this means that the `ErrorHandler` contract is not +involved for reactive methods. + +As a result, further scheduled subscription occurs despite the error. + +In the following example, the `Mono` subscription fails twice in the first five seconds. +Then subscriptions start succeeding, printing a message to the standard output every five +seconds: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Scheduled(initialDelay = 0, fixedRate = 5000) + public Mono reactiveSomething() { + AtomicInteger countdown = new AtomicInteger(2); + + return Mono.defer(() -> { + if (countDown.get() == 0 || countDown.decrementAndGet() == 0) { + return Mono.fromRunnable(() -> System.out.println("Message")); + } + return Mono.error(new IllegalStateException("Cannot deliver message")); + }) + } +---- + +[NOTE] +==== +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 +or even infinite publishers). +==== + [[scheduling-annotation-support-async]] === The `@Async` annotation @@ -587,7 +724,7 @@ In the preceding configuration, a `queue-capacity` value has also been provided. The configuration of the thread pool should also be considered in light of the executor's queue capacity. For the full description of the relationship between pool size and queue capacity, see the documentation for -https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html[`ThreadPoolExecutor`]. +{java-api}/java.base/java/util/concurrent/ThreadPoolExecutor.html[`ThreadPoolExecutor`]. The main idea is that, when a task is submitted, the executor first tries to use a free thread if the number of active threads is currently less than the core size. If the core size has been reached, the task is added to the queue, as long as its @@ -958,7 +1095,7 @@ we need to set up the `SchedulerFactoryBean`, as the following example shows: More properties are available for the `SchedulerFactoryBean`, such as the calendars used by the job details, properties to customize Quartz with, and a Spring-provided JDBC DataSource. See -the {api-spring-framework}/scheduling/quartz/SchedulerFactoryBean.html[`SchedulerFactoryBean`] +the {spring-framework-api}/scheduling/quartz/SchedulerFactoryBean.html[`SchedulerFactoryBean`] javadoc for more information. NOTE: `SchedulerFactoryBean` also recognizes a `quartz.properties` file in the classpath, diff --git a/framework-docs/modules/ROOT/pages/languages/groovy.adoc b/framework-docs/modules/ROOT/pages/languages/groovy.adoc index 745101a99374..e50f136b23bf 100644 --- a/framework-docs/modules/ROOT/pages/languages/groovy.adoc +++ b/framework-docs/modules/ROOT/pages/languages/groovy.adoc @@ -8,7 +8,7 @@ existing Java application. The Spring Framework provides a dedicated `ApplicationContext` that supports a Groovy-based Bean Definition DSL. For more details, see -xref:core/beans/basics.adoc#groovy-bean-definition-dsl[The Groovy Bean Definition DSL]. +xref:core/beans/basics.adoc#beans-factory-groovy[The Groovy Bean Definition DSL]. Further support for Groovy, including beans written in Groovy, refreshable script beans, and more is available in xref:languages/dynamic.adoc[Dynamic Language Support]. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin.adoc index c6beb7f4a6ca..d373497009bc 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin.adoc @@ -2,9 +2,9 @@ = Kotlin :page-section-summary-toc: 1 -https://kotlinlang.org[Kotlin] is a statically typed language that targets the JVM +{kotlin-site}[Kotlin] is a statically typed language that targets the JVM (and other platforms) which allows writing concise and elegant code while providing -very good https://kotlinlang.org/docs/reference/java-interop.html[interoperability] +very good {kotlin-docs}/java-interop.html[interoperability] with existing libraries written in Java. The Spring Framework provides first-class support for Kotlin and lets developers write @@ -13,13 +13,13 @@ Most of the code samples of the reference documentation are provided in Kotlin in addition to Java. The easiest way to build a Spring application with Kotlin is to leverage Spring Boot and -its https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[dedicated Kotlin support]. -https://spring.io/guides/tutorials/spring-boot-kotlin/[This comprehensive tutorial] +its {spring-boot-docs}/boot-features-kotlin.html[dedicated Kotlin support]. +{spring-site-guides}/tutorials/spring-boot-kotlin/[This comprehensive tutorial] will teach you how to build Spring Boot applications with Kotlin using https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io]. Feel free to join the #spring channel of https://slack.kotlinlang.org/[Kotlin Slack] or ask a question with `spring` and `kotlin` as tags on -https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow] if you need support. +{stackoverflow-spring-kotlin-tags}[Stackoverflow] if you need support. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc index 4a724caf0115..813d2c106b3b 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc @@ -1,7 +1,7 @@ [[kotlin-annotations]] = Annotations -The Spring Framework also takes advantage of https://kotlinlang.org/docs/reference/null-safety.html[Kotlin null-safety] +The Spring Framework also takes advantage of {kotlin-docs}/null-safety.html[Kotlin null-safety] to determine if an HTTP parameter is required without having to explicitly define the `required` attribute. That means `@RequestParam name: String?` is treated as not required and, conversely, `@RequestParam name: String` is treated as being required. @@ -20,9 +20,9 @@ type `Car` may or may not exist. The same behavior applies to autowired construc NOTE: If you use bean validation on classes with properties or a primary constructor parameters, you may need to use -https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets[annotation use-site targets], +{kotlin-docs}/annotations.html#annotation-use-site-targets[annotation use-site targets], such as `@field:NotNull` or `@get:Size(min=5, max=15)`, as described in -https://stackoverflow.com/a/35853200/1092077[this Stack Overflow response]. +{stackoverflow-site}/a/35853200/1092077[this Stack Overflow response]. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc index 68949a9bf0bc..c2c2b9f246da 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc @@ -51,7 +51,7 @@ the constructor parameters will be autowired by type: ---- In order to allow a more declarative approach and cleaner syntax, Spring Framework provides -a {docs-spring-framework}/kdoc-api/spring-context/org.springframework.context.support/-bean-definition-dsl/index.html[Kotlin bean definition DSL] +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. @@ -104,10 +104,10 @@ as the following example shows: ---- NOTE: Spring Boot is based on JavaConfig and -https://github.com/spring-projects/spring-boot/issues/8115[does not yet provide specific support for functional bean definition], +{spring-boot-issues}/8115[does not yet provide specific support for functional bean definition], but you can experimentally use functional bean definitions through Spring Boot's `ApplicationContextInitializer` support. -See https://stackoverflow.com/questions/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685[this Stack Overflow answer] -for more details and up-to-date information. See also the experimental Kofu DSL developed in https://github.com/spring-projects/spring-fu[Spring Fu incubator]. +See {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/classes-interfaces.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc index 5dd4520dd9f5..604563704a50 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc @@ -12,7 +12,7 @@ compiler flag to be enabled during compilation. (For completeness, we neverthele running the Kotlin compiler with its `-java-parameters` flag for standard Java parameter exposure.) You can declare configuration classes as -https://kotlinlang.org/docs/reference/nested-classes.html[top level or nested but not inner], +{kotlin-docs}/nested-classes.html[top level or nested but not inner], since the later requires a reference to the outer class. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc index 17becb7aa151..913acb052e71 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc @@ -1,21 +1,23 @@ [[coroutines]] = Coroutines -Kotlin https://kotlinlang.org/docs/reference/coroutines-overview.html[Coroutines] are Kotlin +Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are Kotlin lightweight threads allowing to write non-blocking code in an imperative way. On language side, suspending functions provides an abstraction for asynchronous operations while on library side -https://github.com/Kotlin/kotlinx.coroutines[kotlinx.coroutines] provides functions like -https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html[`async { }`] -and types like https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`]. +{kotlin-github-org}/kotlinx.coroutines[kotlinx.coroutines] provides functions like +{kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines/async.html[`async { }`] +and types like {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`]. Spring Framework provides support for Coroutines on the following scope: -* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html[Deferred] and https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[Flow] return values support in Spring MVC and WebFlux annotated `@Controller` +* {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html[Deferred] and {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[Flow] return values support in Spring MVC and WebFlux annotated `@Controller` * Suspending function support in Spring MVC and WebFlux annotated `@Controller` -* Extensions for WebFlux {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.client/index.html[client] and {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/index.html[server] functional API. -* WebFlux.fn {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL +* Extensions for WebFlux {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.client/index.html[client] and {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/index.html[server] functional API. +* WebFlux.fn {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL +* WebFlux {spring-framework-api-kdoc}/spring-web/org.springframework.web.server/-co-web-filter/index.html[`CoWebFilter`] * Suspending function and `Flow` support in RSocket `@MessageMapping` annotated methods -* Extensions for {docs-spring-framework}/kdoc-api/spring-messaging/org.springframework.messaging.rsocket/index.html[`RSocketRequester`] +* Extensions for {spring-framework-api-kdoc}/spring-messaging/org.springframework.messaging.rsocket/index.html[`RSocketRequester`] +* Spring AOP @@ -53,17 +55,17 @@ For input parameters: * If laziness is not needed, `fun handler(mono: Mono)` becomes `fun handler(value: T)` since a suspending functions can be invoked to get the value parameter. * If laziness is needed, `fun handler(mono: Mono)` becomes `fun handler(supplier: suspend () -> T)` or `fun handler(supplier: suspend () -> T?)` -https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`] is `Flux` equivalent in Coroutines world, suitable for hot or cold stream, finite or infinite streams, with the following main differences: +{kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`] is `Flux` equivalent in Coroutines world, suitable for hot or cold stream, finite or infinite streams, with the following main differences: * `Flow` is push-based while `Flux` is push-pull hybrid * Backpressure is implemented via suspending functions -* `Flow` has only a https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html[single suspending `collect` method] and operators are implemented as https://kotlinlang.org/docs/reference/extensions.html[extensions] -* https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators[Operators are easy to implement] thanks to Coroutines +* `Flow` has only a {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html[single suspending `collect` method] and operators are implemented as {kotlin-docs}/extensions.html[extensions] +* {kotlin-github-org}/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators[Operators are easy to implement] thanks to Coroutines * Extensions allow to add custom operators to `Flow` * Collect operations are suspending functions -* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html[`map` operator] supports asynchronous operation (no need for `flatMap`) since it takes a suspending function parameter +* {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html[`map` operator] supports asynchronous operation (no need for `flatMap`) since it takes a suspending function parameter -Read this blog post about https://spring.io/blog/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow[Going Reactive with Spring, Coroutines and Kotlin Flow] +Read this blog post about {spring-site-blog}/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow[Going Reactive with Spring, Coroutines and Kotlin Flow] for more details, including how to run code concurrently with Coroutines. @@ -170,7 +172,7 @@ class CoroutinesViewController(banner: Banner) { [[webflux-fn]] == WebFlux.fn -Here is an example of Coroutines router defined via the {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL and related handlers. +Here is an example of Coroutines router defined via the {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL and related handlers. [source,kotlin,indent=0] ---- diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc index d7da3aee5328..6af9b086ae9f 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc @@ -1,11 +1,11 @@ [[kotlin-extensions]] = Extensions -Kotlin https://kotlinlang.org/docs/reference/extensions.html[extensions] provide the ability +Kotlin {kotlin-docs}/extensions.html[extensions] provide the ability to extend existing classes with additional functionality. The Spring Framework Kotlin APIs use these extensions to add new Kotlin-specific conveniences to existing Spring APIs. -The {docs-spring-framework}/kdoc-api/[Spring Framework KDoc API] lists +The {spring-framework-api-kdoc}/[Spring Framework KDoc API] lists and documents all available Kotlin extensions and DSLs. NOTE: Keep in mind that Kotlin extensions need to be imported to be used. This means, @@ -13,8 +13,8 @@ for example, that the `GenericApplicationContext.registerBean` Kotlin extension is available only if `org.springframework.context.support.registerBean` is imported. That said, similar to static imports, an IDE should automatically suggest the import in most cases. -For example, https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters[Kotlin reified type parameters] -provide a workaround for JVM https://docs.oracle.com/javase/tutorial/java/generics/erasure.html[generics type erasure], +For example, {kotlin-docs}/inline-functions.html#reified-type-parameters[Kotlin reified type parameters] +provide a workaround for JVM {java-tutorial}/java/generics/erasure.html[generics type erasure], and the Spring Framework provides some extensions to take advantage of this feature. This allows for a better Kotlin API `RestTemplate`, for the new `WebClient` from Spring WebFlux, and for various other APIs. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc index c53a37351ded..6b8b75b491ec 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc @@ -2,7 +2,7 @@ = Getting Started The easiest way to learn how to build a Spring application with Kotlin is to follow -https://spring.io/guides/tutorials/spring-boot-kotlin/[the dedicated tutorial]. +{spring-site-guides}/tutorials/spring-boot-kotlin/[the dedicated tutorial]. @@ -10,22 +10,21 @@ https://spring.io/guides/tutorials/spring-boot-kotlin/[the dedicated tutorial]. == `start.spring.io` The easiest way to start a new Spring Framework project in Kotlin is to create a new Spring -Boot 2 project on https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io]. +Boot project on https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin[start.spring.io]. [[choosing-the-web-flavor]] == Choosing the Web Flavor -Spring Framework now comes with two different web stacks: xref:web/webmvc.adoc#mvc[Spring MVC] and +Spring Framework comes with two different web stacks: xref:web/webmvc.adoc#mvc[Spring MVC] and xref:testing/unit.adoc#mock-objects-web-reactive[Spring WebFlux]. Spring WebFlux is recommended if you want to create applications that will deal with latency, -long-lived connections, streaming scenarios or if you want to use the web functional -Kotlin DSL. +long-lived connections or streaming scenarios. For other use cases, especially if you are using blocking technologies such as JPA, Spring -MVC and its annotation-based programming model is the recommended choice. +MVC is the recommended choice. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc index dc1a3f0257b8..96070d163f42 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc @@ -1,20 +1,20 @@ [[kotlin-null-safety]] = Null-safety -One of Kotlin's key features is https://kotlinlang.org/docs/reference/null-safety.html[null-safety], +One of Kotlin's key features is {kotlin-docs}/null-safety.html[null-safety], which cleanly deals with `null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes applications safer through nullability declarations and expressing "`value or no value`" semantics without paying the cost of wrappers, such as `Optional`. (Kotlin allows using functional constructs with nullable values. See this -https://www.baeldung.com/kotlin-null-safety[comprehensive guide to Kotlin null-safety].) +{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety].) Although Java does not let you express null-safety in its type-system, the Spring Framework provides xref:languages/kotlin/null-safety.adoc[null-safety of the whole Spring Framework API] via tooling-friendly annotations declared in the `org.springframework.lang` package. By default, types from Java APIs used in Kotlin are recognized as -https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types], +{kotlin-docs}/java-interop.html#null-safety-and-platform-types[platform types], for which null-checks are relaxed. -https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support[Kotlin support for JSR-305 annotations] +{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. @@ -30,7 +30,7 @@ API nullability declaration could evolve even between minor releases and that mo be added in the future. NOTE: Generic type arguments, varargs, and array elements nullability are not supported yet, -but should be in an upcoming release. See https://github.com/Kotlin/KEEP/issues/79[this discussion] +but should be in an upcoming release. See {kotlin-github-org}/KEEP/issues/79[this discussion] for up-to-date information. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc index 80a2b48fc15a..d2b3657d3127 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc @@ -2,16 +2,13 @@ = Requirements :page-section-summary-toc: 1 -Spring Framework supports Kotlin 1.3+ and requires +Spring Framework supports Kotlin 1.7+ and requires https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib[`kotlin-stdlib`] -(or one of its variants, such as https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8[`kotlin-stdlib-jdk8`]) and https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect[`kotlin-reflect`] to be present on the classpath. They are provided by default if you bootstrap a Kotlin project on https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io]. -WARNING: Kotlin https://kotlinlang.org/docs/inline-classes.html[inline classes] are not yet supported. - -NOTE: The https://github.com/FasterXML/jackson-module-kotlin[Jackson Kotlin module] is required +NOTE: The {jackson-github-org}/jackson-module-kotlin[Jackson Kotlin module] is required for serializing or deserializing JSON data for Kotlin classes with Jackson, so make sure to add the `com.fasterxml.jackson.module:jackson-module-kotlin` dependency to your project if you have such need. It is automatically registered when found in the classpath. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc index a99666581818..f3be082c275a 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc @@ -4,9 +4,9 @@ We recommend the following resources for people learning how to build applications with Kotlin and the Spring Framework: -* https://kotlinlang.org/docs/reference/[Kotlin language reference] +* {kotlin-docs}[Kotlin language reference] * https://slack.kotlinlang.org/[Kotlin Slack] (with a dedicated #spring channel) -* https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow, with `spring` and `kotlin` tags] +* {stackoverflow-spring-kotlin-tags}[Stackoverflow, with `spring` and `kotlin` tags] * https://play.kotlinlang.org/[Try Kotlin in your browser] * https://blog.jetbrains.com/kotlin/[Kotlin blog] * https://kotlin.link/[Awesome Kotlin] @@ -18,28 +18,9 @@ Kotlin and the Spring Framework: The following Github projects offer examples that you can learn from and possibly even extend: +* https://github.com/spring-guides/tut-spring-boot-kotlin[tut-spring-boot-kotlin]: Sources of {spring-site}/guides/tutorials/spring-boot-kotlin/[the official Spring + Kotlin tutorial] * https://github.com/sdeleuze/spring-boot-kotlin-demo[spring-boot-kotlin-demo]: Regular Spring Boot and Spring Data JPA project -* https://github.com/mixitconf/mixit[mixit]: Spring Boot 2, WebFlux, and Reactive Spring Data MongoDB +* https://github.com/mixitconf/mixit[mixit]: Spring Boot, WebFlux, and Reactive Spring Data MongoDB * https://github.com/sdeleuze/spring-kotlin-functional[spring-kotlin-functional]: Standalone WebFlux and functional bean definition DSL * https://github.com/sdeleuze/spring-kotlin-fullstack[spring-kotlin-fullstack]: WebFlux Kotlin fullstack example with Kotlin2js for frontend instead of JavaScript or TypeScript * https://github.com/spring-petclinic/spring-petclinic-kotlin[spring-petclinic-kotlin]: Kotlin version of the Spring PetClinic Sample Application -* https://github.com/sdeleuze/spring-kotlin-deepdive[spring-kotlin-deepdive]: A step-by-step migration guide for Boot 1.0 and Java to Boot 2.0 and Kotlin -* https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample[spring-cloud-gcp-kotlin-app-sample]: Spring Boot with Google Cloud Platform Integrations - - - -[[issues]] -== Issues - -The following list categorizes the pending issues related to Spring and Kotlin support: - -* Spring Framework -** https://github.com/spring-projects/spring-framework/issues/20606[Unable to use WebTestClient with mock server in Kotlin] -** https://github.com/spring-projects/spring-framework/issues/20496[Support null-safety at generics, varargs and array elements level] -* Kotlin -** https://youtrack.jetbrains.com/issue/KT-6380[Parent issue for Spring Framework support] -** https://youtrack.jetbrains.com/issue/KT-5464[Kotlin requires type inference where Java doesn't] -** https://youtrack.jetbrains.com/issue/KT-20283[Smart cast regression with open classes] -** https://youtrack.jetbrains.com/issue/KT-14984[Impossible to pass not all SAM argument as function] -** https://youtrack.jetbrains.com/issue/KT-15125[Support JSR 223 bindings directly via script variables] -** https://youtrack.jetbrains.com/issue/KT-6653[Kotlin properties do not override Java-style getters and setters] diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc index 3fcc9d51a62a..300c0089c897 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc @@ -9,7 +9,7 @@ in Kotlin. [[final-by-default]] == Final by Default -By default, https://discuss.kotlinlang.org/t/classes-final-by-default/166[all classes in Kotlin are `final`]. +By default, https://discuss.kotlinlang.org/t/classes-final-by-default/166[all classes and member functions in Kotlin are `final`]. The `open` modifier on a class is the opposite of Java's `final`: It allows others to inherit from this class. This also applies to member functions, in that they need to be marked as `open` to be overridden. @@ -21,10 +21,10 @@ member function of Spring beans that are proxied by CGLIB, which can quickly become painful and is against the Kotlin principle of keeping code concise and predictable. NOTE: It is also possible to avoid CGLIB proxies for configuration classes by using `@Configuration(proxyBeanMethods = false)`. -See {api-spring-framework}/context/annotation/Configuration.html#proxyBeanMethods--[`proxyBeanMethods` Javadoc] for more details. +See {spring-framework-api}/context/annotation/Configuration.html#proxyBeanMethods--[`proxyBeanMethods` Javadoc] for more details. Fortunately, Kotlin provides a -https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin[`kotlin-spring`] +{kotlin-docs}/compiler-plugins.html#kotlin-spring-compiler-plugin[`kotlin-spring`] plugin (a preconfigured version of the `kotlin-allopen` plugin) that automatically opens classes and their member functions for types that are annotated or meta-annotated with one of the following annotations: @@ -38,6 +38,12 @@ Meta-annotation support means that types annotated with `@Configuration`, `@Cont `@RestController`, `@Service`, or `@Repository` are automatically opened since these annotations are meta-annotated with `@Component`. +WARNING: Some use cases involving proxies and automatic generation of final methods by the Kotlin compiler require extra +care. For example, a Kotlin class with properties will generate related `final` getters and setters. In order +to be able to proxy related methods, a type level `@Component` annotation should be preferred to method level `@Bean` in +order to have those methods opened by the `kotlin-spring` plugin. A typical use case is `@Scope` and its popular +`@RequestScope` specialization. + https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io] enables the `kotlin-spring` plugin by default. So, in practice, you can write your Kotlin beans without any additional `open` keyword, as in Java. @@ -59,7 +65,7 @@ within the primary constructor, as in the following example: class Person(val name: String, val age: Int) ---- -You can optionally add https://kotlinlang.org/docs/reference/data-classes.html[the `data` keyword] +You can optionally add {kotlin-docs}/data-classes.html[the `data` keyword] to make the compiler automatically derive the following members from all properties declared in the primary constructor: @@ -80,12 +86,12 @@ As the following example shows, this allows for easy changes to individual prope Common persistence technologies (such as JPA) require a default constructor, preventing this kind of design. Fortunately, there is a workaround for this -https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell["`default constructor hell`"], -since Kotlin provides a https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-jpa-compiler-plugin[`kotlin-jpa`] +{stackoverflow-questions}/32038177/kotlin-with-jpa-default-constructor-hell["`default constructor hell`"], +since Kotlin provides a {kotlin-docs}/compiler-plugins.html#kotlin-jpa-compiler-plugin[`kotlin-jpa`] plugin that generates synthetic no-arg constructor for classes annotated with JPA annotations. If you need to leverage this kind of mechanism for other persistence technologies, you can configure -the https://kotlinlang.org/docs/reference/compiler-plugins.html#how-to-use-no-arg-plugin[`kotlin-noarg`] +the {kotlin-docs}/compiler-plugins.html#how-to-use-no-arg-plugin[`kotlin-noarg`] plugin. NOTE: As of the Kay release train, Spring Data supports Kotlin immutable class instances and @@ -97,8 +103,11 @@ does not require the `kotlin-noarg` plugin if the module uses Spring Data object [[injecting-dependencies]] == Injecting Dependencies +[[favor-constructor-injection]] +=== Favor constructor injection + Our recommendation is to try to favor constructor injection with `val` read-only (and -non-nullable when possible) https://kotlinlang.org/docs/reference/properties.html[properties], +non-nullable when possible) {kotlin-docs}/properties.html[properties], as the following example shows: [source,kotlin,indent=0] @@ -130,20 +139,54 @@ as the following example shows: } ---- +[[internal-functions-name-mangling]] +=== Internal functions name mangling +Kotlin functions with the `internal` {kotlin-docs}/visibility-modifiers.html#class-members[visibility modifier] have +their names mangled when compiled to JVM bytecode, which has a side effect when injecting dependencies by name. + +For example, this Kotlin class: +[source,kotlin,indent=0] +---- +@Configuration +class SampleConfiguration { + + @Bean + internal fun sampleBean() = SampleBean() +} +---- + +Translates to this Java representation of the compiled JVM bytecode: +[source,java,indent=0] +---- +@Configuration +@Metadata(/* ... */) +public class SampleConfiguration { + + @Bean + @NotNull + public SampleBean sampleBean$demo_kotlin_internal_test() { + return new SampleBean(); + } +} +---- + +As a consequence, the related bean name represented as a Kotlin string is `"sampleBean\$demo_kotlin_internal_test"`, +instead of `"sampleBean"` for the regular `public` function use-case. Make sure to use the mangled name when injecting +such bean by name, or add `@JvmName("sampleBean")` to disable name mangling. [[injecting-configuration-properties]] == Injecting Configuration Properties In Java, you can inject configuration properties by using annotations (such as pass:q[`@Value("${property}")`)]. However, in Kotlin, `$` is a reserved character that is used for -https://kotlinlang.org/docs/reference/idioms.html#string-interpolation[string interpolation]. +{kotlin-docs}/idioms.html#string-interpolation[string interpolation]. Therefore, if you wish to use the `@Value` annotation in Kotlin, you need to escape the `$` character by writing pass:q[`@Value("\${property}")`]. NOTE: If you use Spring Boot, you should probably use -https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties[`@ConfigurationProperties`] +{spring-boot-docs}/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties[`@ConfigurationProperties`] instead of `@Value` annotations. As an alternative, you can customize the property placeholder prefix by declaring the @@ -177,14 +220,14 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl [[checked-exceptions]] == Checked Exceptions -Java and https://kotlinlang.org/docs/reference/exceptions.html[Kotlin exception handling] +Java and {kotlin-docs}/exceptions.html[Kotlin exception handling] are pretty close, with the main difference being that Kotlin treats all exceptions as unchecked exceptions. However, when using proxied objects (for example classes or methods annotated with `@Transactional`), checked exceptions thrown will be wrapped by default in an `UndeclaredThrowableException`. To get the original exception thrown like in Java, methods should be annotated with -https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-throws/index.html[`@Throws`] +{kotlin-api}/jvm/stdlib/kotlin.jvm/-throws/index.html[`@Throws`] to specify explicitly the checked exceptions thrown (for example `@Throws(IOException::class)`). @@ -194,7 +237,7 @@ to specify explicitly the checked exceptions thrown (for example `@Throws(IOExce Kotlin annotations are mostly similar to Java annotations, but array attributes (which are extensively used in Spring) behave differently. As explained in the -https://kotlinlang.org/docs/reference/annotations.html[Kotlin documentation] you can omit +{kotlin-docs}/annotations.html[Kotlin documentation] you can omit the `value` attribute name, unlike other attributes, and specify it as a `vararg` parameter. To understand what that means, consider `@RequestMapping` (which is one of the most widely @@ -240,15 +283,15 @@ be matched, not only the `GET` method. == Declaration-site variance Dealing with generic types in Spring applications written in Kotlin may require, for some use cases, to understand -Kotlin https://kotlinlang.org/docs/generics.html#declaration-site-variance[declaration-site variance] +Kotlin {kotlin-docs}/generics.html#declaration-site-variance[declaration-site variance] which allows to define the variance when declaring a type, which is not possible in Java which supports only use-site variance. For example, declaring `List` in Kotlin is conceptually equivalent to `java.util.List` because `kotlin.collections.List` is declared as -https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/[`interface List : kotlin.collections.Collection`]. +{kotlin-api}/jvm/stdlib/kotlin.collections/-list/[`interface List : kotlin.collections.Collection`]. -This needs to be taken in account by using the `out` Kotlin keyword on generic types when using Java classes, +This needs to be taken into account by using the `out` Kotlin keyword on generic types when using Java classes, for example when writing a `org.springframework.core.convert.converter.Converter` from a Kotlin type to a Java type. [source,kotlin,indent=0] @@ -267,7 +310,7 @@ class ListOfAnyConverter : Converter, CustomJavaList<*>> { ---- NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans, -subscribe to https://github.com/spring-projects/spring-framework/issues/22313[spring-framework#22313] to track related +subscribe to {spring-framework-issues}/22313[spring-framework#22313] to track related progresses. @@ -280,18 +323,21 @@ The recommended testing framework is https://junit.org/junit5/[JUnit 5] along wi https://mockk.io/[Mockk] for mocking. NOTE: If you are using Spring Boot, see -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-kotlin-testing[this related documentation]. +{spring-boot-docs}/features.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 5 allows constructor injection of beans which is pretty useful with Kotlin +JUnit Jupiter (JUnit 5) allows constructor injection of beans which is pretty useful with Kotlin in order to use `val` instead of `lateinit var`. You can use -{api-spring-framework}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] +{spring-framework-api}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] to enable autowiring for all parameters. +NOTE: You can also change the default behavior to `ALL` in a `junit-platform.properties` +file with a `spring.test.constructor.autowire.mode = all` property. + [source,kotlin,indent=0] ---- @SpringJUnitConfig(TestConfig::class) @@ -308,11 +354,11 @@ class OrderServiceIntegrationTests(val orderService: OrderService, === `PER_CLASS` Lifecycle Kotlin lets you specify meaningful test function names between backticks (```). -As of JUnit 5, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` +With JUnit Jupiter (JUnit 5), 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. -You can also change the default behavior to `PER_CLASS` thanks to a `junit-platform.properties` +NOTE: You can also change the default behavior to `PER_CLASS` in a `junit-platform.properties` file with a `junit.jupiter.testinstance.lifecycle.default = per_class` property. The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on non-static methods: @@ -380,15 +426,3 @@ class SpecificationLikeTests { ---- -[[kotlin-webtestclient-issue]] -=== `WebTestClient` Type Inference Issue in Kotlin - -Due to a https://youtrack.jetbrains.com/issue/KT-5464[type inference issue], you must -use the Kotlin `expectBody` extension (such as `.expectBody().isEqualTo("toys")`), -since it provides a workaround for the Kotlin issue with the Java API. - -See also the related https://jira.spring.io/browse/SPR-16057[SPR-16057] issue. - - - - diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc index 0b44ce3c7432..e594069b0d4a 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc @@ -8,9 +8,9 @@ Spring Framework comes with a Kotlin router DSL available in 3 flavors: -* WebMvc.fn DSL with {docs-spring-framework}/kdoc-api/spring-webmvc/org.springframework.web.servlet.function/router.html[router { }] -* WebFlux.fn <> DSL with {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/router.html[router { }] -* WebFlux.fn <> DSL with {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] +* xref:web/webmvc-functional.adoc[WebMvc.fn DSL] with {spring-framework-api-kdoc}/spring-webmvc/org.springframework.web.servlet.function/router.html[router { }] +* xref:web/webflux-functional.adoc[WebFlux.fn Reactive DSL] with {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/router.html[router { }] +* xref:languages/kotlin/coroutines.adoc[WebFlux.fn Coroutines DSL] with {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] These DSL let you write clean and idiomatic Kotlin code to build a `RouterFunction` instance as the following example shows: @@ -79,12 +79,12 @@ mockMvc.get("/person/{name}", "Lee") { == Kotlin Script Templates Spring Framework provides a -https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html[`ScriptTemplateView`] -which supports https://www.jcp.org/en/jsr/detail?id=223[JSR-223] to render templates by using script engines. +{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 -https://github.com/Kotlin/kotlinx.html[kotlinx.html] DSL or Kotlin multiline interpolated `String`. +{kotlin-github-org}/kotlinx.html[kotlinx.html] DSL or Kotlin multiline interpolated `String`. `build.gradle.kts` [source,kotlin,indent=0] @@ -126,10 +126,10 @@ project for more details. [[kotlin-multiplatform-serialization]] == Kotlin multiplatform serialization -As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is +{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 https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. +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, diff --git a/framework-docs/modules/ROOT/pages/overview.adoc b/framework-docs/modules/ROOT/pages/overview.adoc index b0ed76a7d869..cb03d79d9c0d 100644 --- a/framework-docs/modules/ROOT/pages/overview.adoc +++ b/framework-docs/modules/ROOT/pages/overview.adoc @@ -57,18 +57,18 @@ competition with Spring, they are in fact complementary. The Spring programming model does not embrace the Jakarta EE platform specification; rather, it integrates with carefully selected individual specifications from the traditional EE umbrella: -* Servlet API (https://jcp.org/en/jsr/detail?id=340[JSR 340]) -* WebSocket API (https://www.jcp.org/en/jsr/detail?id=356[JSR 356]) -* Concurrency Utilities (https://www.jcp.org/en/jsr/detail?id=236[JSR 236]) -* JSON Binding API (https://jcp.org/en/jsr/detail?id=367[JSR 367]) -* Bean Validation (https://jcp.org/en/jsr/detail?id=303[JSR 303]) -* JPA (https://jcp.org/en/jsr/detail?id=338[JSR 338]) -* JMS (https://jcp.org/en/jsr/detail?id=914[JSR 914]) +* Servlet API ({JSR}340[JSR 340]) +* WebSocket API ({JSR}356[JSR 356]) +* Concurrency Utilities ({JSR}236[JSR 236]) +* JSON Binding API ({JSR}367[JSR 367]) +* Bean Validation ({JSR}303[JSR 303]) +* JPA ({JSR}338[JSR 338]) +* JMS ({JSR}914[JSR 914]) * as well as JTA/JCA setups for transaction coordination, if necessary. The Spring Framework also supports the Dependency Injection -(https://www.jcp.org/en/jsr/detail?id=330[JSR 330]) and Common Annotations -(https://jcp.org/en/jsr/detail?id=250[JSR 250]) specifications, which application +({JSR}330[JSR 330]) and Common Annotations +({JSR}250[JSR 250]) specifications, which application developers may choose to use instead of the Spring-specific mechanisms provided by the Spring Framework. Originally, those were based on common `javax` packages. @@ -89,7 +89,7 @@ and can run on servers (such as Netty) that are not Servlet containers. Spring continues to innovate and to evolve. Beyond the Spring Framework, there are other projects, such as Spring Boot, Spring Security, Spring Data, Spring Cloud, Spring Batch, among others. It’s important to remember that each project has its own source code repository, -issue tracker, and release cadence. See https://spring.io/projects[spring.io/projects] for +issue tracker, and release cadence. See {spring-site-projects}[spring.io/projects] for the complete list of Spring projects. @@ -125,17 +125,17 @@ clean code structure with no circular dependencies between packages. == Feedback and Contributions For how-to questions or diagnosing or debugging issues, we suggest using Stack Overflow. Click -https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-r2dbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux+or+spring-rsocket?tab=Newest[here] +{stackoverflow-spring-tag}+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-r2dbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux+or+spring-rsocket?tab=Newest[here] for a list of the suggested tags to use on Stack Overflow. If you're fairly certain that there is a problem in the Spring Framework or would like to suggest a feature, please use -the https://github.com/spring-projects/spring-framework/issues[GitHub Issues]. +the {spring-framework-issues}[GitHub Issues]. If you have a solution in mind or a suggested fix, you can submit a pull request on -https://github.com/spring-projects/spring-framework[Github]. However, please keep in mind +{spring-framework-github}[Github]. However, please keep in mind that, for all but the most trivial issues, we expect a ticket to be filed in the issue tracker, where discussions take place and leave a record for future reference. -For more details see the guidelines at the {spring-framework-main-code}/CONTRIBUTING.md[CONTRIBUTING], +For more details see the guidelines at the {spring-framework-code}/CONTRIBUTING.md[CONTRIBUTING], top-level project page. @@ -145,15 +145,15 @@ top-level project page. == Getting Started If you are just getting started with Spring, you may want to begin using the Spring -Framework by creating a https://projects.spring.io/spring-boot/[Spring Boot]-based +Framework by creating a {spring-site-projects}/spring-boot/[Spring Boot]-based application. Spring Boot provides a quick (and opinionated) way to create a production-ready Spring-based application. It is based on the Spring Framework, favors convention over configuration, and is designed to get you up and running as quickly as possible. You can use https://start.spring.io/[start.spring.io] to generate a basic project or follow -one of the https://spring.io/guides["Getting Started" guides], such as -https://spring.io/guides/gs/rest-service/[Getting Started Building a RESTful Web Service]. +one of the {spring-site-guides}["Getting Started" guides], such as +{spring-site-guides}/gs/rest-service/[Getting Started Building a RESTful Web Service]. As well as being easier to digest, these guides are very task focused, and most of them are based on Spring Boot. They also cover other projects from the Spring portfolio that you might want to consider when solving a particular problem. diff --git a/framework-docs/modules/ROOT/pages/rsocket.adoc b/framework-docs/modules/ROOT/pages/rsocket.adoc index a65f52b125d8..402c213898df 100644 --- a/framework-docs/modules/ROOT/pages/rsocket.adoc +++ b/framework-docs/modules/ROOT/pages/rsocket.adoc @@ -23,7 +23,7 @@ while the above interactions are called "request streams" or simply "requests". These are the key features and benefits of the RSocket protocol: -* https://www.reactive-streams.org/[Reactive Streams] semantics across network boundary -- +* {reactive-streams-site}/[Reactive Streams] semantics across network boundary -- for streaming requests such as `Request-Stream` and `Channel`, back pressure signals travel between requester and responder, allowing a requester to slow down a responder at the source, hence reducing reliance on network layer congestion control, and the need @@ -38,9 +38,9 @@ the amount of state required. * Fragmentation and re-assembly of large messages. * Keepalive (heartbeats). -RSocket has {gh-rsocket}[implementations] in multiple languages. The -{gh-rsocket-java}[Java library] is built on https://projectreactor.io/[Project Reactor], -and https://github.com/reactor/reactor-netty[Reactor Netty] for the transport. That means +RSocket has {rsocket-github-org}[implementations] in multiple languages. The +{rsocket-java}[Java library] is built on {reactor-site}/[Project Reactor], +and {reactor-github-org}/reactor-netty[Reactor Netty] for the transport. That means signals from Reactive Streams Publishers in your application propagate transparently through RSocket across the network. @@ -50,8 +50,8 @@ through RSocket across the network. === The Protocol One of the benefits of RSocket is that it has well defined behavior on the wire and an -easy to read https://rsocket.io/about/protocol[specification] along with some protocol -{gh-rsocket}/rsocket/tree/master/Extensions[extensions]. Therefore it is +easy to read {rsocket-site}/about/protocol[specification] along with some protocol +{rsocket-protocol-extensions}[extensions]. Therefore it is a good idea to read the spec, independent of language implementations and higher level framework APIs. This section provides a succinct overview to establish some context. @@ -96,18 +96,18 @@ and therefore only included in the first message on a request, i.e. with one of Protocol extensions define common metadata formats for use in applications: -* {gh-rsocket-extensions}/CompositeMetadata.md[Composite Metadata]-- multiple, +* {rsocket-protocol-extensions}/CompositeMetadata.md[Composite Metadata]-- multiple, independently formatted metadata entries. -* {gh-rsocket-extensions}/Routing.md[Routing] -- the route for a request. +* {rsocket-protocol-extensions}/Routing.md[Routing] -- the route for a request. [[rsocket-java]] === Java Implementation -The {gh-rsocket-java}[Java implementation] for RSocket is built on -https://projectreactor.io/[Project Reactor]. The transports for TCP and WebSocket are -built on https://github.com/reactor/reactor-netty[Reactor Netty]. As a Reactive Streams +The {rsocket-java}[Java implementation] for RSocket is built on +{reactor-site}/[Project Reactor]. The transports for TCP and WebSocket are +built on {reactor-github-org}/reactor-netty[Reactor Netty]. As a Reactive Streams library, Reactor simplifies the job of implementing the protocol. For applications it is a natural fit to use `Flux` and `Mono` with declarative operators and transparent back pressure support. @@ -117,7 +117,7 @@ features and leaves the application programming model (e.g. RPC codegen vs other higher level, independent concern. The main contract -{gh-rsocket-java}/blob/master/rsocket-core/src/main/java/io/rsocket/RSocket.java[io.rsocket.RSocket] +{rsocket-java-code}/rsocket-core/src/main/java/io/rsocket/RSocket.java[io.rsocket.RSocket] models the four request interaction types with `Mono` representing a promise for a single message, `Flux` a stream of messages, and `io.rsocket.Payload` the actual message with access to data and metadata as byte buffers. The `RSocket` contract is used @@ -127,7 +127,7 @@ requests with. For responding, the application implements `RSocket` to handle re This is not meant to be a thorough introduction. For the most part, Spring applications will not have to use its API directly. However it may be important to see or experiment with RSocket independent of Spring. The RSocket Java repository contains a number of -{gh-rsocket-java}/tree/master/rsocket-examples[sample apps] that +{rsocket-java-code}/rsocket-examples[sample apps] that demonstrate its API and protocol features. @@ -137,10 +137,12 @@ demonstrate its API and protocol features. The `spring-messaging` module contains the following: -* xref:rsocket.adoc#rsocket-requester[RSocketRequester] -- fluent API to make requests through an `io.rsocket.RSocket` - with data and metadata encoding/decoding. -* xref:rsocket.adoc#rsocket-annot-responders[Annotated Responders] -- `@MessageMapping` annotated handler methods for - responding. +* xref:rsocket.adoc#rsocket-requester[RSocketRequester] -- fluent API to make requests +through an `io.rsocket.RSocket` with data and metadata encoding/decoding. +* xref:rsocket.adoc#rsocket-annot-responders[Annotated Responders] -- `@MessageMapping` + and `@RSocketExchange` annotated handler methods for responding. +* xref:rsocket.adoc#rsocket-interface[RSocket Interface] -- RSocket service declaration +as Java interface with `@RSocketExchange` methods, for use as requester or responder. The `spring-web` module contains `Encoder` and `Decoder` implementations such as Jackson CBOR/JSON, and Protobuf that RSocket applications will likely need. It also contains the @@ -150,7 +152,7 @@ Spring Boot 2.2 supports standing up an RSocket server over TCP or WebSocket, in the option to expose RSocket over WebSocket in a WebFlux server. There is also client support and auto-configuration for an `RSocketRequester.Builder` and `RSocketStrategies`. See the -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-rsocket[RSocket section] +{spring-boot-docs}/messaging.html#messaging.rsocket[RSocket section] in the Spring Boot reference for more details. Spring Security 5.2 provides RSocket support. @@ -220,7 +222,7 @@ established transparently and used. For data, the default mime type is derived from the first configured `Decoder`. For metadata, the default mime type is -{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] which allows multiple +{rsocket-protocol-extensions}/CompositeMetadata.md[composite metadata] which allows multiple metadata value and mime type pairs per request. Typically both don't need to be changed. Data and metadata in the `SETUP` frame is optional. On the server side, @@ -532,7 +534,7 @@ Kotlin:: ====== Extra metadata values can be added if using -{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] (the default) and if the +{rsocket-protocol-extensions}/CompositeMetadata.md[composite metadata] (the default) and if the values are supported by a registered `Encoder`. For example: [tabs] @@ -661,8 +663,8 @@ Kotlin:: ====== `RSocketMessageHandler` supports -{gh-rsocket-extensions}/CompositeMetadata.md[composite] and -{gh-rsocket-extensions}/Routing.md[routing] metadata by default. You can set its +{rsocket-protocol-extensions}/CompositeMetadata.md[composite] and +{rsocket-protocol-extensions}/Routing.md[routing] metadata by default. You can set its xref:rsocket.adoc#rsocket-metadata-extractor[MetadataExtractor] if you need to switch to a different mime type or register additional metadata mime types. @@ -863,6 +865,69 @@ interaction type(s): +[[rsocket-annot-rsocketexchange]] +=== @RSocketExchange + +As an alternative to `@MessageMapping`, you can also handle requests with +`@RSocketExchange` methods. Such methods are declared on an +xref:rsocket-interface[RSocket Interface] and can be used as a requester via +`RSocketServiceProxyFactory` or implemented by a responder. + +For example, to handle requests as a responder: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + public interface RadarsService { + + @RSocketExchange("locate.radars.within") + Flux radars(MapRequest request); + } + + @Controller + public class RadarsController implements RadarsService { + + public Flux radars(MapRequest request) { + // ... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + interface RadarsService { + + @RSocketExchange("locate.radars.within") + fun radars(request: MapRequest): Flow + } + + @Controller + class RadarsController : RadarsService { + + override fun radars(request: MapRequest): Flow { + // ... + } + } +---- +====== + +There some differences between `@RSocketExhange` and `@MessageMapping` since the +former needs to remain suitable for requester and responder use. For example, while +`@MessageMapping` can be declared to handle any number of routes and each route can +be a pattern, `@RSocketExchange` must be declared with a single, concrete route. There are +also small differences in the supported method parameters related to metadata, see +xref:rsocket-annot-messagemapping[@MessageMapping] and +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 @@ -889,7 +954,7 @@ xref:rsocket.adoc#rsocket-requester-server[Server Requester] for details. == MetadataExtractor Responders must interpret metadata. -{gh-rsocket-extensions}/CompositeMetadata.md[Composite metadata] allows independently +{rsocket-protocol-extensions}/CompositeMetadata.md[Composite metadata] allows independently formatted metadata values (e.g. 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. @@ -900,7 +965,7 @@ in annotated handler methods. `DefaultMetadataExtractor` can be given `Decoder` instances to decode metadata. Out of the box it has built-in support for -{gh-rsocket-extensions}/Routing.md["message/x.rsocket.routing.v0"] which it decodes to +{rsocket-protocol-extensions}/Routing.md["message/x.rsocket.routing.v0"] which it decodes to `String` and saves under the "route" key. For any other mime type you'll need to provide a `Decoder` and register the mime type as follows: @@ -997,12 +1062,13 @@ Kotlin:: [[rsocket-interface]] == RSocket Interface -The Spring Framework lets you define an RSocket service as a Java interface with annotated -methods for RSocket exchanges. You can then generate a proxy that implements this interface -and performs the exchanges. This helps to simplify RSocket remote access by wrapping the -use of the underlying xref:rsocket.adoc#rsocket-requester[RSocketRequester]. +The Spring Framework lets you define an RSocket service as a Java interface with +`@RSocketExchange` methods. You can pass such an interface to `RSocketServiceProxyFactory` +to create a proxy which performs requests through an +xref:rsocket.adoc#rsocket-requester[RSocketRequester]. You can also implement the +interface as a responder that handles requests. -One, declare an interface with `@RSocketExchange` methods: +Start by creating the interface with `@RSocketExchange` methods: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -1016,7 +1082,7 @@ One, declare an interface with `@RSocketExchange` methods: } ---- -Two, create a proxy that will perform the declared RSocket exchanges: +Now you can create a proxy that performs requests when methods are called: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -1026,6 +1092,10 @@ Two, create a proxy that will perform the declared RSocket exchanges: RadarService service = factory.createClient(RadarService.class); ---- +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 @@ -1066,3 +1136,10 @@ method parameters: Annotated, RSocket exchange methods support return values that are concrete value(s), or any producer of value(s) that can be adapted to a Reactive Streams `Publisher` via `ReactiveAdapterRegistry`. + +By default, the behavior of RSocket service methods with synchronous (blocking) method +signature depends on response timeout settings of the underlying RSocket `ClientTransport` +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/annotations/integration-junit-jupiter.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc index 2876f194df48..4944779c3b44 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc @@ -11,6 +11,7 @@ xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupite * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[`@NestedTestConfiguration`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-enabledif[`@EnabledIf`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-disabledif[`@DisabledIf`] +* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`] [[integration-testing-annotations-junit-jupiter-springjunitconfig]] == `@SpringJUnitConfig` @@ -81,7 +82,7 @@ Kotlin:: See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] as well as the javadoc for -{api-spring-framework}/test/context/junit/jupiter/SpringJUnitConfig.html[`@SpringJUnitConfig`] +{spring-framework-api}/test/context/junit/jupiter/SpringJUnitConfig.html[`@SpringJUnitConfig`] and `@ContextConfiguration` for further details. [[integration-testing-annotations-junit-jupiter-springjunitwebconfig]] @@ -156,9 +157,9 @@ Kotlin:: See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] as well as the javadoc for -{api-spring-framework}/test/context/junit/jupiter/web/SpringJUnitWebConfig.html[`@SpringJUnitWebConfig`], -{api-spring-framework}/test/context/ContextConfiguration.html[`@ContextConfiguration`], and -{api-spring-framework}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] +{spring-framework-api}/test/context/junit/jupiter/web/SpringJUnitWebConfig.html[`@SpringJUnitWebConfig`], +{spring-framework-api}/test/context/ContextConfiguration.html[`@ContextConfiguration`], and +{spring-framework-api}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] for further details. [[integration-testing-annotations-testconstructor]] @@ -170,8 +171,9 @@ of a test class constructor are autowired from components in the test's 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` on a -constructor takes precedence over both `@TestConstructor` and the default mode. +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. .Changing the default test constructor autowire mode [TIP] @@ -222,6 +224,7 @@ following annotations. * xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[`@ContextConfiguration`] * xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[`@WebAppConfiguration`] * xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[`@ContextHierarchy`] +* xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[`@ContextCustomizerFactories`] * xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[`@ActiveProfiles`] * xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`] * xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[`@DynamicPropertySource`] 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 1e992c75e865..78164ab07744 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc @@ -11,6 +11,7 @@ xref:testing/testcontext-framework.adoc[TestContext framework]. * `@BootstrapWith` * `@ContextConfiguration` * `@ContextHierarchy` +* `@ContextCustomizerFactories` * `@ActiveProfiles` * `@TestPropertySource` * `@DirtiesContext` @@ -304,5 +305,5 @@ Kotlin:: ====== For further details, see the -https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] +{spring-framework-wiki}/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] wiki page. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc index 3db79d44bc4d..3804efbc56f7 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc @@ -12,6 +12,7 @@ Spring's testing annotations include the following: * xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[`@ContextConfiguration`] * xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[`@WebAppConfiguration`] * xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[`@ContextHierarchy`] +* xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[`@ContextCustomizerFactories`] * xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[`@ActiveProfiles`] * xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`] * xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[`@DynamicPropertySource`] @@ -26,4 +27,5 @@ Spring's testing annotations include the following: * xref:testing/annotations/integration-spring/annotation-sqlconfig.adoc[`@SqlConfig`] * xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode`] * xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[`@SqlGroup`] +* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`] diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-activeprofiles.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-activeprofiles.adoc index 6b3d521b492b..31299b96bd07 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 @@ -74,6 +74,6 @@ and registering it by using the `resolver` attribute of `@ActiveProfiles`. See xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[Context Configuration with Environment Profiles], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration], and the -{api-spring-framework}/test/context/ActiveProfiles.html[`@ActiveProfiles`] javadoc for +{spring-framework-api}/test/context/ActiveProfiles.html[`@ActiveProfiles`] javadoc for examples and further details. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc new file mode 100644 index 000000000000..0dc49e7bec52 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc @@ -0,0 +1,45 @@ +[[spring-testing-annotation-contextcustomizerfactories]] += `@ContextCustomizerFactories` + +`@ContextCustomizerFactories` is used to register `ContextCustomizerFactory` +implementations for a particular test class, its subclasses, and its nested classes. If +you wish to register a factory globally, you should register it via the automatic +discovery mechanism described in +xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[`ContextCustomizerFactory` Configuration]. + +The following example shows how to register two `ContextCustomizerFactory` implementations: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @ContextConfiguration + @ContextCustomizerFactories({CustomContextCustomizerFactory.class, AnotherContextCustomizerFactory.class}) // <1> + class CustomContextCustomizerFactoryTests { + // class body... + } +---- +<1> Register two `ContextCustomizerFactory` implementations. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @ContextConfiguration + @ContextCustomizerFactories([CustomContextCustomizerFactory::class, AnotherContextCustomizerFactory::class]) // <1> + class CustomContextCustomizerFactoryTests { + // class body... + } +---- +<1> Register two `ContextCustomizerFactory` implementations. +====== + + +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. + diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contexthierarchy.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contexthierarchy.adoc index f136a4da5c99..66aedbcb9fd0 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 @@ -70,6 +70,6 @@ If you need to merge or override the configuration for a given level of the cont hierarchy within a test class hierarchy, you must explicitly name that level by supplying the same value to the `name` attribute in `@ContextConfiguration` at each corresponding level in the class hierarchy. See xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[Context Hierarchies] and the -{api-spring-framework}/test/context/ContextHierarchy.html[`@ContextHierarchy`] javadoc +{spring-framework-api}/test/context/ContextHierarchy.html[`@ContextHierarchy`] javadoc for further examples. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dirtiescontext.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dirtiescontext.adoc index 39c27a8f1600..12d361deca8a 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 @@ -257,6 +257,6 @@ Kotlin:: For further details regarding the `EXHAUSTIVE` and `CURRENT_LEVEL` algorithms, see the -{api-spring-framework}/test/annotation/DirtiesContext.HierarchyMode.html[`DirtiesContext.HierarchyMode`] +{spring-framework-api}/test/annotation/DirtiesContext.HierarchyMode.html[`DirtiesContext.HierarchyMode`] javadoc. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-disabledinaotmode.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-disabledinaotmode.adoc new file mode 100644 index 000000000000..2fbfc6a19b33 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-disabledinaotmode.adoc @@ -0,0 +1,20 @@ +[[spring-testing-annotation-disabledinaotmode]] += `@DisabledInAotMode` + +`@DisabledInAotMode` signals that an annotated test class is disabled in Spring AOT +(ahead-of-time) mode, which means that the `ApplicationContext` for the test class will +not be processed for AOT optimizations at build time. + +If a test class is annotated with `@DisabledInAotMode`, all other test classes which +specify configuration to load the same `ApplicationContext` must also be annotated with +`@DisabledInAotMode`. Failure to annotate all such test classes will result in an +exception, either at build time or run time. + +When used with JUnit Jupiter based tests, `@DisabledInAotMode` also signals that the +annotated test class or test method is disabled when running the test suite in Spring AOT +mode. When applied at the class level, all test methods within that class will be +disabled. In this sense, `@DisabledInAotMode` has semantics similar to those of JUnit +Jupiter's `@DisabledInNativeImage` annotation. + +For details on AOT support specific to integration tests, see +xref:testing/testcontext-framework/aot.adoc[Ahead of Time Support for Tests]. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc index 003175517ccf..4c6e37863c36 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 @@ -6,7 +6,7 @@ _dynamic_ properties to be added to the set of `PropertySources` in the `Environ an `ApplicationContext` loaded for an integration test. Dynamic properties are useful when you do not know the value of the properties upfront – for example, if the properties are managed by an external resource such as for a container managed by the -https://www.testcontainers.org/[Testcontainers] project. +{testcontainers-site}[Testcontainers] project. The following example demonstrates how to register a dynamic property: diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-recordapplicationevents.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-recordapplicationevents.adoc index fbd7f0cf03f2..73365f75e1bd 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 @@ -9,6 +9,6 @@ _Spring TestContext Framework_ to record all application events that are publish The recorded events can be accessed via the `ApplicationEvents` API within tests. See xref:testing/testcontext-framework/application-events.adoc[Application Events] and the -{api-spring-framework}/test/context/event/RecordApplicationEvents.html[`@RecordApplicationEvents` +{spring-framework-api}/test/context/event/RecordApplicationEvents.html[`@RecordApplicationEvents` javadoc] for an example and further details. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc index 3e9505d15235..370d1e7d5865 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 @@ -39,7 +39,7 @@ Kotlin:: By default, `@TestExecutionListeners` provides support for inheriting listeners from superclasses or enclosing classes. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] and the -{api-spring-framework}/test/context/TestExecutionListeners.html[`@TestExecutionListeners` +{spring-framework-api}/test/context/TestExecutionListeners.html[`@TestExecutionListeners` javadoc] for an example and further details. If you discover that you need to switch back to using the default `TestExecutionListener` implementations, see the note in xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-registering-tels[Registering `TestExecutionListener` Implementations]. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-webappconfiguration.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-webappconfiguration.adoc index a367a0ee9451..2c3d1ac328dc 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 @@ -80,6 +80,6 @@ Kotlin:: Note that `@WebAppConfiguration` must be used in conjunction with `@ContextConfiguration`, either within a single test class or within a test class hierarchy. See the -{api-spring-framework}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] +{spring-framework-api}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] javadoc for further details. diff --git a/framework-docs/modules/ROOT/pages/testing/resources.adoc b/framework-docs/modules/ROOT/pages/testing/resources.adoc index c7b3c247dc10..9b6b5d61b99f 100644 --- a/framework-docs/modules/ROOT/pages/testing/resources.adoc +++ b/framework-docs/modules/ROOT/pages/testing/resources.adoc @@ -8,7 +8,7 @@ See the following resources for more information about testing: * https://testng.org/[TestNG]: A testing framework inspired by JUnit with added support for test groups, data-driven testing, distributed testing, and other features. Supported in the xref:testing/testcontext-framework.adoc[Spring TestContext Framework] -* https://assertj.github.io/doc/[AssertJ]: "Fluent assertions for Java", +* {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 @@ -24,7 +24,7 @@ See the following resources for more information about testing: * https://www.dbunit.org/[DbUnit]: JUnit extension (also usable with Ant and Maven) that is targeted at database-driven projects and, among other things, puts your database into a known state between test runs. -* https://www.testcontainers.org/[Testcontainers]: Java library that supports JUnit +* {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. diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc index a223a9f4ca35..e8c56ed11fc0 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 @@ -211,5 +211,5 @@ configuration. Check for the support for code completion on static members. == Further Examples of Client-side REST Tests Spring MVC Test's own tests include -{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/client/samples[example +{spring-framework-code}/spring-test/src/test/java/org/springframework/test/web/client/samples[example tests] of client-side REST tests. diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-defining-expectations.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-defining-expectations.adoc index e581e32b1905..27dbf6ed50f5 100644 --- 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 @@ -184,7 +184,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== @@ -192,7 +192,7 @@ Note that common expectations are always applied and cannot be overridden withou creating a separate `MockMvc` instance. When a JSON response content contains hypermedia links created with -https://github.com/spring-projects/spring-hateoas[Spring HATEOAS], you can verify the +{spring-github-org}/spring-hateoas[Spring HATEOAS], you can verify the resulting links by using JsonPath expressions, as the following example shows: [tabs] @@ -220,7 +220,7 @@ Kotlin:: ====== When XML response content contains hypermedia links created with -https://github.com/spring-projects/spring-hateoas[Spring HATEOAS], you can verify the +{spring-github-org}/spring-hateoas[Spring HATEOAS], you can verify the resulting links by using XPath expressions: [tabs] 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 index 7b379b88cde5..b292ec168a3e 100644 --- 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 @@ -18,7 +18,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc index 145fbb6b809d..7ef8a8dbcfbf 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc @@ -113,7 +113,7 @@ Kotlin:: ====== Finally, we can verify that a new message was created successfully. The following -assertions use the https://assertj.github.io/doc/[AssertJ] library: +assertions use the {assertj-docs}[AssertJ] library: [tabs] ====== @@ -268,7 +268,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc index 392ae27a8cbd..2c9bff2f937d 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc @@ -362,7 +362,7 @@ annotation to look up our submit button with a `css` selector (*input[type=submi -- Finally, we can verify that a new message was created successfully. The following -assertions use the https://assertj.github.io/doc/[AssertJ] assertion library: +assertions use the {assertj-docs}[AssertJ] assertion library: -- [tabs] @@ -562,7 +562,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== 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 index ba8ac3772dc3..0d6bcab7ca33 100644 --- 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 @@ -159,7 +159,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== 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 index 7444c1038fb1..cb9c8e97e8d6 100644 --- 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 @@ -3,9 +3,9 @@ :page-section-summary-toc: 1 The framework's own tests include -{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples[ +{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-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client[ +{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 index b38d69ed683e..2abf6919d5a5 100644 --- 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 @@ -111,7 +111,8 @@ a mock service with Mockito: [source,xml,indent=0,subs="verbatim,quotes"] ---- - + + ---- @@ -152,7 +153,7 @@ Kotlin:: @Autowired lateinit var accountService: AccountService - lateinit mockMvc: MockMvc + lateinit var mockMvc: MockMvc @BeforeEach fun setup(wac: WebApplicationContext) { 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 index 2ec41f725a57..e179a8364a27 100644 --- 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 @@ -25,7 +25,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== @@ -53,11 +53,11 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== See the javadoc for -{api-spring-framework}/test/web/servlet/setup/ConfigurableMockMvcBuilder.html[`ConfigurableMockMvcBuilder`] +{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/vs-end-to-end-integration-tests.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc index 9b26c80f2370..9b3d38ccff7d 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/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc @@ -1,7 +1,7 @@ [[spring-mvc-test-vs-end-to-end-integration-tests]] = MockMvc vs End-to-End Tests -MockMVc is built on Servlet API mock implementations from the +MockMvc is built on Servlet API mock implementations from the `spring-test` module and does not rely on a running container. Therefore, there are some differences when compared to full end-to-end integration tests with an actual client and a live server running. @@ -21,7 +21,7 @@ for rendering JSON, XML, and other formats through `@ResponseBody` methods. Alternatively, you may consider the full end-to-end integration testing support from Spring Boot with `@SpringBootTest`. See the -{docs-spring-boot}/html/spring-boot-features.html#boot-features-testing[Spring Boot Reference Guide]. +{spring-boot-docs}/spring-boot-features.html#boot-features-testing[Spring Boot Reference Guide]. There are pros and cons for each approach. The options provided in Spring MVC Test are different stops on the scale from classic unit testing to full integration testing. To be diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc index c5c3e7dc5a2c..6a24b6ca7c45 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc @@ -1,38 +1,16 @@ [[spring-mvc-test-vs-streaming-response]] = Streaming Responses -The best way to test streaming responses such as Server-Sent Events is through the -<> which can be used as a test client to connect to a `MockMvc` instance -to perform tests on Spring MVC controllers without a running server. For example: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build(); - - FluxExchangeResult exchangeResult = client.get() - .uri("/persons") - .exchange() - .expectStatus().isOk() - .expectHeader().contentType("text/event-stream") - .returnResult(Person.class); - - // Use StepVerifier from Project Reactor to test the streaming response - - StepVerifier.create(exchangeResult.getResponseBody()) - .expectNext(new Person("N0"), new Person("N1"), new Person("N2")) - .expectNextCount(4) - .consumeNextWith(person -> assertThat(person.getName()).endsWith("7")) - .thenCancel() - .verify(); ----- -====== - -`WebTestClient` can also connect to a live server and perform full end-to-end integration -tests. This is also supported in Spring Boot where you can -{docs-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server]. +You can use `WebTestClient` to test xref:testing/webtestclient.adoc#webtestclient-stream[streaming responses] +such as Server-Sent Events. However, `MockMvcWebTestClient` doesn't support infinite +streams because there is no way to cancel the server stream from the client side. +To test infinite streams, you'll need to +xref:testing/webtestclient.adoc#webtestclient-server-config[bind to] a running server, +or when using Spring Boot, +{spring-boot-docs}/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-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/testcontext-framework/aot.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc index 67f9894db69d..5348b383c4cf 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc @@ -19,22 +19,46 @@ following features. use an AOT-optimized `ApplicationContext` that participates transparently with the xref:testing/testcontext-framework/ctx-management/caching.adoc[context cache]. -[WARNING] +All tests are enabled in AOT mode by default. However, you can selectively disable an +entire test class or individual test method in AOT mode by annotating it with +xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`]. +When using JUnit Jupiter, you may selectively enable or disable tests in a GraalVM native +image via Jupiter's `@EnabledInNativeImage` and `@DisabledInNativeImage` annotations. +Note that `@DisabledInAotMode` also disables the annotated test class or test method when +running within a GraalVM native image, analogous to JUnit Jupiter's +`@DisabledInNativeImage` annotation. + +[TIP] +==== +By default, if an error is encountered during build-time AOT processing, an exception +will be thrown, and the overall process will fail immediately. + +If you would prefer that build-time AOT processing continue after errors are encountered, +you can disable the `failOnError` mode which results in errors being logged at `WARN` +level or with greater detail at `DEBUG` level. + +The `failOnError` mode can be disabled from the command line or a build script by setting +a JVM system property named `spring.test.aot.processing.failOnError` to `false`. As an +alternative, you can set the same property via the +xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. +==== + +[NOTE] ==== -The `@ContextHierarchy` annotation is currently not supported in AOT mode. +The `@ContextHierarchy` annotation is not supported in AOT mode. ==== To provide test-specific runtime hints for use within a GraalVM native image, you have the following options. * Implement a custom - {api-spring-framework}/test/context/aot/TestRuntimeHintsRegistrar.html[`TestRuntimeHintsRegistrar`] + {spring-framework-api}/test/context/aot/TestRuntimeHintsRegistrar.html[`TestRuntimeHintsRegistrar`] and register it globally via `META-INF/spring/aot.factories`. -* Implement a custom {api-spring-framework}/aot/hint/RuntimeHintsRegistrar.html[`RuntimeHintsRegistrar`] +* Implement a custom {spring-framework-api}/aot/hint/RuntimeHintsRegistrar.html[`RuntimeHintsRegistrar`] and register it globally via `META-INF/spring/aot.factories` or locally on a test class - via {api-spring-framework}/context/annotation/ImportRuntimeHints.html[`@ImportRuntimeHints`]. -* Annotate a test class with {api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] or - {api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`]. + via {spring-framework-api}/context/annotation/ImportRuntimeHints.html[`@ImportRuntimeHints`]. +* Annotate a test class with {spring-framework-api}/aot/hint/annotation/Reflective.html[`@Reflective`] or + {spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`]. * See xref:core/aot.adoc#aot.hints[Runtime Hints] for details on Spring's core runtime hints and annotation support. @@ -47,12 +71,12 @@ that are not specific to particular test classes, favor implementing ==== If you implement a custom `ContextLoader`, it must implement -{api-spring-framework}/test/context/aot/AotContextLoader.html[`AotContextLoader`] in +{spring-framework-api}/test/context/aot/AotContextLoader.html[`AotContextLoader`] in order to provide AOT build-time processing and AOT runtime execution support. Note, however, that all context loader implementations provided by the Spring Framework and Spring Boot already implement `AotContextLoader`. If you implement a custom `TestExecutionListener`, it must implement -{api-spring-framework}/test/context/aot/AotTestExecutionListener.html[`AotTestExecutionListener`] +{spring-framework-api}/test/context/aot/AotTestExecutionListener.html[`AotTestExecutionListener`] in order to participate in AOT processing. See the `SqlScriptsTestExecutionListener` in the `spring-test` module for an example. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc index 14cf022a8891..b27d2d98c5b5 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 @@ -24,7 +24,7 @@ To use `ApplicationEvents` in your tests, do the following. to an `@Autowired` field in the test class. The following test class uses the `SpringExtension` for JUnit Jupiter and -https://assertj.github.io/doc/[AssertJ] to assert the types of application events +{assertj-docs}[AssertJ] to assert the types of application events published while invoking a method in a Spring-managed component: // Don't use "quotes" in the "subs" section because of the asterisks in /* ... */ @@ -88,6 +88,6 @@ Kotlin:: ====== See the -{api-spring-framework}/test/context/event/ApplicationEvents.html[`ApplicationEvents` +{spring-framework-api}/test/context/event/ApplicationEvents.html[`ApplicationEvents` javadoc] for further details regarding the `ApplicationEvents` API. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc index 30ce7bd59df4..9aa446ed035e 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 @@ -112,6 +112,7 @@ advanced use cases. * xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts] * 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/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] * xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[Context Configuration with Environment Profiles] @@ -119,5 +120,6 @@ advanced use cases. * xref:testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc[Context Configuration with Dynamic Property Sources] * xref:testing/testcontext-framework/ctx-management/web.adoc[Loading a `WebApplicationContext`] * 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 0a4f02f91206..a75d6314aab7 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 @@ -20,7 +20,7 @@ framework uses the following configuration parameters to build the context cache * `contextLoader` (from `@ContextConfiguration`) * `parent` (from `@ContextHierarchy`) * `activeProfiles` (from `@ActiveProfiles`) -* `propertySourceLocations` (from `@TestPropertySource`) +* `propertySourceDescriptors` (from `@TestPropertySource`) * `propertySourceProperties` (from `@TestPropertySource`) * `resourceBasePath` (from `@WebAppConfiguration`) 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 new file mode 100644 index 000000000000..1698c6169291 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc @@ -0,0 +1,71 @@ +[[testcontext-context-customizers]] += Configuration Configuration with Context Customizers + +A `ContextCustomizer` is responsible for customizing the supplied +`ConfigurableApplicationContext` after bean definitions have been loaded into the context +but before the context has been refreshed. + +A `ContextCustomizerFactory` is responsible for creating a `ContextCustomizer`, based on +some custom logic which determines if the `ContextCustomizer` is necessary for a given +test class -- for example, based on the presence of a certain annotation. Factories are +invoked after `ContextLoaders` have processed context configuration attributes for a test +class but before the `MergedContextConfiguration` is created. + +For example, Spring Framework provides the following `ContextCustomizerFactory` +implementation which is registered by default: + +`MockServerContainerContextCustomizerFactory`:: Creates a + `MockServerContainerContextCustomizer` if WebSocket support is present in the classpath + and the test class or one of its enclosing classes is annotated or meta-annotated with + `@WebAppConfiguration`. `MockServerContainerContextCustomizer` instantiates a new + `MockServerContainer` and stores it in the `ServletContext` under the attribute named + `jakarta.websocket.server.ServerContainer`. + + +[[testcontext-context-customizers-registration]] +== Registering `ContextCustomizerFactory` Implementations + +You can register `ContextCustomizerFactory` implementations explicitly for a test class, its +subclasses, and its nested classes by using the `@ContextCustomizerFactories` annotation. See +xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[annotation support] +and the javadoc for +{spring-framework-api}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories`] +for details and examples. + + +[[testcontext-context-customizers-automatic-discovery]] +== Automatic Discovery of Default `ContextCustomizerFactory` Implementations + +Registering `ContextCustomizerFactory` implementations by using `@ContextCustomizerFactories` is +suitable for custom factories that are used in limited testing scenarios. However, it can +become cumbersome if a custom factory needs to be used across an entire test suite. This +issue is addressed through support for automatic discovery of default +`ContextCustomizerFactory` implementations through the `SpringFactoriesLoader` mechanism. + +For example, the modules that make up the testing support in Spring Framework and Spring +Boot declare all core default `ContextCustomizerFactory` implementations under the +`org.springframework.test.context.ContextCustomizerFactory` key in their +`META-INF/spring.factories` properties files. The `spring.factories` file for the +`spring-test` module can be viewed +{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[here]. +Third-party frameworks and developers can contribute their own `ContextCustomizerFactory` +implementations to the list of default factories in the same manner through their own +`spring.factories` files. + + +[[testcontext-context-customizers-merging]] +== Merging `ContextCustomizerFactory` Implementations + +If a custom `ContextCustomizerFactory` is registered via `@ContextCustomizerFactories`, it +will be _merged_ with the default factories that have been registered using the aforementioned +xref:testing/testcontext-framework/ctx-management/context-customizers.adoc#testcontext-context-customizers-automatic-discovery[automatic discovery mechanism]. + +The merging algorithm ensures that duplicates are removed from the list and that locally +declared factories are appended to the list of default factories when merged. + +[TIP] +==== +To replace the default factories for a test class, its subclasses, and its nested +classes, you can set the `mergeMode` attribute of `@ContextCustomizerFactories` to +`MergeMode.REPLACE_DEFAULTS`. +==== diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc index ad418ef4cdfc..4705d57cbe64 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 @@ -11,7 +11,7 @@ integration test. ==== The `@DynamicPropertySource` annotation and its supporting infrastructure were originally designed to allow properties from -https://www.testcontainers.org/[Testcontainers] based tests to be exposed easily to +{testcontainers-site}[Testcontainers] based tests to be exposed easily to Spring integration tests. However, this feature may also be used with any form of external resource whose lifecycle is maintained outside the test's `ApplicationContext`. ==== diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/env-profiles.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/env-profiles.adoc index bb868bf5fae4..f1d52d53c03a 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 @@ -478,7 +478,7 @@ programmatically instead of declaratively -- for example, based on: To resolve active bean definition profiles programmatically, you can implement a custom `ActiveProfilesResolver` and register it by using the `resolver` attribute of `@ActiveProfiles`. For further information, see the corresponding -{api-spring-framework}/test/context/ActiveProfilesResolver.html[javadoc]. +{spring-framework-api}/test/context/ActiveProfilesResolver.html[javadoc]. The following example demonstrates how to implement and register a custom `OperatingSystemActiveProfilesResolver`: diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/failure-threshold.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/failure-threshold.adoc new file mode 100644 index 000000000000..b4cda2450118 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/failure-threshold.adoc @@ -0,0 +1,23 @@ +[[testcontext-ctx-management-failure-threshold]] += Context Failure Threshold + +As of Spring Framework 6.1, a context _failure threshold_ policy is in place which helps +avoid repeated attempts to load a failing `ApplicationContext`. By default, the failure +threshold is set to `1` which means that only one attempt will be made to load an +`ApplicationContext` for a given context cache key (see +xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]). Any +subsequent attempt to load the `ApplicationContext` for the same context cache key will +result in an immediate `IllegalStateException` with an error message which explains that +the attempt was preemptively skipped. This behavior allows individual test classes and +test suites to fail faster by avoiding repeated attempts to load an `ApplicationContext` +that will never successfully load -- for example, due to a configuration error or a missing +external resource that prevents the context from loading in the current environment. + +You can configure the context failure threshold from the command line or a build script +by setting a JVM system property named `spring.test.context.failure.threshold` with a +positive integer value. As an alternative, you can set the same property via the +xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. + +NOTE: If you wish to effectively disable the context failure threshold, you can set the +property to a very large value. For example, from the command line you could set the +system property via `-Dspring.test.context.failure.threshold=1000000`. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc index 9049bfc9ee22..443a89fb38db 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc @@ -2,7 +2,7 @@ = Context Configuration with Groovy Scripts To load an `ApplicationContext` for your tests by using Groovy scripts that use the -xref:core/beans/basics.adoc#groovy-bean-definition-dsl[Groovy Bean Definition DSL], you can annotate +xref:core/beans/basics.adoc#beans-factory-groovy[Groovy Bean Definition DSL], you can annotate your test class with `@ContextConfiguration` and configure the `locations` or `value` attribute with an array that contains the resource locations of Groovy scripts. Resource lookup semantics for Groovy scripts are the same as those described for diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc index c92ebb9064b2..22953ed289cb 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 @@ -235,6 +235,6 @@ NOTE: If you use `@DirtiesContext` in a test whose context is configured as part context hierarchy, you can use the `hierarchyMode` flag to control how the context cache is cleared. For further details, see the discussion of `@DirtiesContext` in xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] and the -{api-spring-framework}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc. +{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc. -- diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/javaconfig.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/javaconfig.adoc index cfd0cedfe235..af460ea84f06 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 @@ -51,8 +51,8 @@ The term "`component class`" can refer to any of the following: of a single constructor without the use of Spring annotations. See the javadoc of -{api-spring-framework}/context/annotation/Configuration.html[`@Configuration`] and -{api-spring-framework}/context/annotation/Bean.html[`@Bean`] for further information +{spring-framework-api}/context/annotation/Configuration.html[`@Configuration`] and +{spring-framework-api}/context/annotation/Bean.html[`@Bean`] for further information regarding the configuration and semantics of component classes, paying special attention to the discussion of `@Bean` Lite Mode. ==== @@ -62,7 +62,7 @@ TestContext framework tries to detect the presence of default configuration clas Specifically, `AnnotationConfigContextLoader` and `AnnotationConfigWebContextLoader` detect all `static` nested classes of the test class that meet the requirements for configuration class implementations, as specified in the -{api-spring-framework}/context/annotation/Configuration.html[`@Configuration`] javadoc. +{spring-framework-api}/context/annotation/Configuration.html[`@Configuration`] javadoc. Note that the name of the configuration class is arbitrary. In addition, a test class can contain more than one `static` nested configuration class if desired. In the following example, the `OrderServiceTest` class declares a `static` nested configuration class 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 491e7e9ac679..34057860b856 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 @@ -16,7 +16,7 @@ SPI, but `@TestPropertySource` is not supported with implementations of the olde `ContextLoader` SPI. Implementations of `SmartContextLoader` gain access to merged test property source values -through the `getPropertySourceLocations()` and `getPropertySourceProperties()` methods in +through the `getPropertySourceDescriptors()` and `getPropertySourceProperties()` methods in `MergedContextConfiguration`. ==== @@ -26,17 +26,23 @@ through the `getPropertySourceLocations()` and `getPropertySourceProperties()` m You can configure test properties files by using the `locations` or `value` attribute of `@TestPropertySource`. -Both traditional and XML-based properties file formats are supported -- for example, -`"classpath:/com/example/test.properties"` or `"file:///path/to/file.xml"`. +By default, both traditional and XML-based `java.util.Properties` file formats are +supported -- for example, `"classpath:/com/example/test.properties"` or +`"file:///path/to/file.xml"`. As of Spring Framework 6.1, you can configure a custom +`PropertySourceFactory` via the `factory` attribute in `@TestPropertySource` in order to +support a different file format such as JSON, YAML, etc. Each path is interpreted as a Spring `Resource`. A plain path (for example, `"test.properties"`) is treated as a classpath resource that is relative to the package in which the test class is defined. A path starting with a slash is treated as an absolute classpath resource (for example: `"/org/example/test.xml"`). A path that references a URL (for example, a path prefixed with `classpath:`, `file:`, or `http:`) is -loaded by using the specified resource protocol. Resource location wildcards (such as -`{asterisk}{asterisk}/{asterisk}.properties`) are not permitted: Each location must -evaluate to exactly one `.properties` or `.xml` resource. +loaded by using the specified resource protocol. + +Property placeholders in paths (such as `${...}`) will be resolved against the `Environment`. + +As of Spring Framework 6.1, resource location patterns are also supported — for +example, `"classpath*:/config/*.properties"`. The following example uses a test properties file: @@ -80,6 +86,20 @@ a Java properties file: * `key:value` * `key value` +[TIP] +==== +Although properties can be defined using any of the above syntax variants and any number +of spaces between the key and the value, it is recommended that you use one syntax +variant and consistent spacing within your test suite — for example, consider always +using `key = value` instead of `key= value`, `key=value`, etc. Similarly, if you define +inlined properties using text blocks you should consistently use text blocks for inlined +properties throughout your test suite. + +The reason is that the exact strings you provide will be used to determine the key for +the context cache. Consequently, to benefit from the context cache you must ensure that +you define inlined properties consistently. +==== + The following example sets two inlined properties: [tabs] @@ -89,24 +109,61 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- @ContextConfiguration - @TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) // <1> + @TestPropertySource(properties = {"timezone = GMT", "port = 4242"}) // <1> + class MyIntegrationTests { + // class body... + } +---- +<1> Setting two properties via an array of strings. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @ContextConfiguration + @TestPropertySource(properties = ["timezone = GMT", "port = 4242"]) // <1> + class MyIntegrationTests { + // class body... + } +---- +<1> Setting two properties via an array of strings. +====== + +As of Spring Framework 6.1, you can use _text blocks_ to define multiple inlined +properties in a single `String`. The following example sets two inlined properties using +a text block: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @ContextConfiguration + @TestPropertySource(properties = """ + timezone = GMT + port = 4242 + """) // <1> class MyIntegrationTests { // class body... } ---- -<1> Setting two properties by using two variations of the key-value syntax. +<1> Setting two properties via a text block. Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- @ContextConfiguration - @TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) // <1> + @TestPropertySource(properties = [""" + timezone = GMT + port = 4242 + """]) // <1> class MyIntegrationTests { // class body... } ---- -<1> Setting two properties by using two variations of the key-value syntax. +<1> Setting two properties via a text block. ====== [NOTE] @@ -166,7 +223,7 @@ Java:: @ContextConfiguration @TestPropertySource( locations = "/test.properties", - properties = {"timezone = GMT", "port: 4242"} + properties = {"timezone = GMT", "port = 4242"} ) class MyIntegrationTests { // class body... @@ -179,7 +236,7 @@ Kotlin:: ---- @ContextConfiguration @TestPropertySource("/test.properties", - properties = ["timezone = GMT", "port: 4242"] + properties = ["timezone = GMT", "port = 4242"] ) class MyIntegrationTests { // class body... diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc index dc51cfa9d0dc..b3bd66ff7c7f 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc @@ -29,7 +29,7 @@ integration test methods. scripts and is mainly intended for internal use within the framework. However, if you require full control over how SQL scripts are parsed and run, `ScriptUtils` may suit your needs better than some of the other alternatives described later. See the -{api-spring-framework}/jdbc/datasource/init/ScriptUtils.html[javadoc] for individual +{spring-framework-api}/jdbc/datasource/init/ScriptUtils.html[javadoc] for individual methods in `ScriptUtils` for further details. `ResourceDatabasePopulator` provides an object-based API for programmatically populating, @@ -38,7 +38,7 @@ resources. `ResourceDatabasePopulator` provides options for configuring the char encoding, statement separator, comment delimiters, and error handling flags used when parsing and running the scripts. Each of the configuration options has a reasonable default value. See the -{api-spring-framework}/jdbc/datasource/init/ResourceDatabasePopulator.html[javadoc] for +{spring-framework-api}/jdbc/datasource/init/ResourceDatabasePopulator.html[javadoc] for details on default values. To run the scripts configured in a `ResourceDatabasePopulator`, you can invoke either the `populate(Connection)` method to run the populator against a `java.sql.Connection` or the `execute(DataSource)` method @@ -95,13 +95,22 @@ In addition to the aforementioned mechanisms for running SQL scripts programmati you can declaratively configure SQL scripts in the Spring TestContext Framework. Specifically, you can declare the `@Sql` annotation on a test class or test method to configure individual SQL statements or the resource paths to SQL scripts that should be -run against a given database before or after an integration test method. Support for -`@Sql` is provided by the `SqlScriptsTestExecutionListener`, which is enabled by default. - -NOTE: Method-level `@Sql` declarations override class-level declarations by default. As -of Spring Framework 5.2, however, this behavior may be configured per test class or per -test method via `@SqlMergeMode`. See -xref:testing/testcontext-framework/executing-sql.adoc#testcontext-executing-sql-declaratively-script-merging[Merging and Overriding Configuration with `@SqlMergeMode`] for further details. +run against a given database before or after an integration test class or test method. +Support for `@Sql` is provided by the `SqlScriptsTestExecutionListener`, which is enabled +by default. + +[NOTE] +==== +Method-level `@Sql` declarations override class-level declarations by default, but this +behavior may be configured per test class or per test method via `@SqlMergeMode`. See +xref:testing/testcontext-framework/executing-sql.adoc#testcontext-executing-sql-declaratively-script-merging[Merging and Overriding Configuration with `@SqlMergeMode`] +for further details. + +However, this does not apply to class-level declarations configured for the +`BEFORE_TEST_CLASS` or `AFTER_TEST_CLASS` execution phases. Such declarations cannot be +overridden, and the corresponding scripts and statements will be executed once per class +in addition to any method-level scripts and statements. +==== [[testcontext-executing-sql-declaratively-script-resources]] === Path Resource Semantics @@ -174,17 +183,25 @@ script, depending on where `@Sql` is declared. If a default cannot be detected, defined in the class `com.example.MyTest`, the corresponding default script is `classpath:com/example/MyTest.testMethod.sql`. +[[testcontext-executing-sql-declaratively-logging]] +=== Logging SQL Scripts and Statements + +If you want to see which SQL scripts are being executed, set the +`org.springframework.test.context.jdbc` logging category to `DEBUG`. + +If you want to see which SQL statements are being executed, set the +`org.springframework.jdbc.datasource.init` logging category to `DEBUG`. + [[testcontext-executing-sql-declaratively-multiple-annotations]] === Declaring Multiple `@Sql` Sets If you need to configure multiple sets of SQL scripts for a given test class or test method but with different syntax configuration, different error handling rules, or -different execution phases per set, you can declare multiple instances of `@Sql`. With -Java 8, you can use `@Sql` as a repeatable annotation. Otherwise, you can use the -`@SqlGroup` annotation as an explicit container for declaring multiple instances of -`@Sql`. +different execution phases per set, you can declare multiple instances of `@Sql`. You can +either use `@Sql` as a repeatable annotation, or you can use the `@SqlGroup` annotation +as an explicit container for declaring multiple instances of `@Sql`. -The following example shows how to use `@Sql` as a repeatable annotation with Java 8: +The following example shows how to use `@Sql` as a repeatable annotation: [tabs] ====== @@ -204,7 +221,12 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin + @Test + @Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")) + @Sql("/test-user-data.sql") + fun userTest() { + // run code that uses the test schema and test data + } ---- ====== @@ -212,9 +234,8 @@ In the scenario presented in the preceding example, the `test-schema.sql` script different syntax for single-line comments. The following example is identical to the preceding example, except that the `@Sql` -declarations are grouped together within `@SqlGroup`. With Java 8 and above, the use of -`@SqlGroup` is optional, but you may need to use `@SqlGroup` for compatibility with -other JVM languages such as Kotlin. +declarations are grouped together within `@SqlGroup`. The use of `@SqlGroup` is optional, +but you may need to use `@SqlGroup` for compatibility with other JVM languages. [tabs] ====== @@ -239,7 +260,8 @@ Kotlin:: @Test @SqlGroup( Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")), - Sql("/test-user-data.sql")) + Sql("/test-user-data.sql") + ) fun userTest() { // Run code that uses the test schema and test data } @@ -249,10 +271,10 @@ Kotlin:: [[testcontext-executing-sql-declaratively-script-execution-phases]] === Script Execution Phases -By default, SQL scripts are run before the corresponding test method. However, if -you need to run a particular set of scripts after the test method (for example, to clean -up database state), you can use the `executionPhase` attribute in `@Sql`, as the -following example shows: +By default, SQL scripts are run before the corresponding test method. However, if you +need to run a particular set of scripts after the test method (for example, to clean up +database state), you can set the `executionPhase` attribute in `@Sql` to +`AFTER_TEST_METHOD`, as the following example shows: [tabs] ====== @@ -281,12 +303,11 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- @Test - @SqlGroup( - Sql("create-test-data.sql", - config = SqlConfig(transactionMode = ISOLATED)), - Sql("delete-test-data.sql", - config = SqlConfig(transactionMode = ISOLATED), - executionPhase = AFTER_TEST_METHOD)) + @Sql("create-test-data.sql", + config = SqlConfig(transactionMode = ISOLATED)) + @Sql("delete-test-data.sql", + config = SqlConfig(transactionMode = ISOLATED), + executionPhase = AFTER_TEST_METHOD) fun userTest() { // run code that needs the test data to be committed // to the database outside of the test's transaction @@ -294,9 +315,60 @@ Kotlin:: ---- ====== -Note that `ISOLATED` and `AFTER_TEST_METHOD` are statically imported from +NOTE: `ISOLATED` and `AFTER_TEST_METHOD` are statically imported from `Sql.TransactionMode` and `Sql.ExecutionPhase`, respectively. +As of Spring Framework 6.1, it is possible to run a particular set of scripts before or +after the test class by setting the `executionPhase` attribute in a class-level `@Sql` +declaration to `BEFORE_TEST_CLASS` or `AFTER_TEST_CLASS`, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @SpringJUnitConfig + @Sql(scripts = "/test-schema.sql", executionPhase = BEFORE_TEST_CLASS) + class DatabaseTests { + + @Test + void emptySchemaTest() { + // run code that uses the test schema without any test data + } + + @Test + @Sql("/test-user-data.sql") + void userTest() { + // run code that uses the test schema and test data + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @SpringJUnitConfig + @Sql("/test-schema.sql", executionPhase = BEFORE_TEST_CLASS) + class DatabaseTests { + + @Test + fun emptySchemaTest() { + // run code that uses the test schema without any test data + } + + @Test + @Sql("/test-user-data.sql") + fun userTest() { + // run code that uses the test schema and test data + } + } +---- +====== + +NOTE: `BEFORE_TEST_CLASS` is statically imported from `Sql.ExecutionPhase`. + [[testcontext-executing-sql-declaratively-script-configuration]] === Script Configuration with `@SqlConfig` @@ -320,11 +392,11 @@ local `@SqlConfig` attributes do not supply an explicit value other than `""`, ` The configuration options provided by `@Sql` and `@SqlConfig` are equivalent to those supported by `ScriptUtils` and `ResourceDatabasePopulator` but are a superset of those provided by the `` XML namespace element. See the javadoc of -individual attributes in {api-spring-framework}/test/context/jdbc/Sql.html[`@Sql`] and -{api-spring-framework}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] for details. +individual attributes in {spring-framework-api}/test/context/jdbc/Sql.html[`@Sql`] and +{spring-framework-api}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] for details. [[testcontext-executing-sql-declaratively-tx]] -*Transaction management for `@Sql`* +==== Transaction management for `@Sql` By default, the `SqlScriptsTestExecutionListener` infers the desired transaction semantics for scripts configured by using `@Sql`. Specifically, SQL scripts are run @@ -343,8 +415,8 @@ behavior by setting the `transactionMode` attribute of `@SqlConfig` (for example scripts should be run in an isolated transaction). Although a thorough discussion of all supported options for transaction management with `@Sql` is beyond the scope of this reference manual, the javadoc for -{api-spring-framework}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] and -{api-spring-framework}/test/context/jdbc/SqlScriptsTestExecutionListener.html[`SqlScriptsTestExecutionListener`] +{spring-framework-api}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] and +{spring-framework-api}/test/context/jdbc/SqlScriptsTestExecutionListener.html[`SqlScriptsTestExecutionListener`] provide detailed information, and the following example shows a typical testing scenario that uses JUnit Jupiter and transactional tests with `@Sql`: diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc index 04e5e9ce4aa8..6910ce06fb9e 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc @@ -10,7 +10,7 @@ in turn, manages a `TestContext` that holds the context of the current test. The and delegates to `TestExecutionListener` implementations, which instrument the actual test execution by providing dependency injection, managing transactions, and so on. A `SmartContextLoader` is responsible for loading an `ApplicationContext` for a given test -class. See the {api-spring-framework}/test/context/package-summary.html[javadoc] and the +class. See the {spring-framework-api}/test/context/package-summary.html[javadoc] and the Spring test suite for further information and examples of various implementations. [[testcontext]] diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc index 1a3c642f6261..6cb00cbcc75f 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 @@ -40,7 +40,7 @@ for details. WARNING: Parallel test execution in the Spring TestContext Framework is only possible if the underlying `TestContext` implementation provides a copy constructor, as explained in -the javadoc for {api-spring-framework}/test/context/TestContext.html[`TestContext`]. The +the javadoc for {spring-framework-api}/test/context/TestContext.html[`TestContext`]. The `DefaultTestContext` used in Spring provides such a constructor. However, if you use a third-party library that provides a custom `TestContext` implementation, you need to verify that it is suitable for parallel test execution. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc index 7f0a8c19aa23..51731166829f 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 @@ -177,7 +177,7 @@ following features above and beyond the feature set that Spring supports for JUn TestNG: * Dependency injection for test constructors, test methods, and test lifecycle callback - methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with `SpringExtension`] for further details. + methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details. * Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional test execution] based on SpEL expressions, environment variables, system properties, and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in @@ -310,17 +310,19 @@ See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details. [[testcontext-junit-jupiter-di]] -=== Dependency Injection with `SpringExtension` +=== Dependency Injection with the `SpringExtension` -`SpringExtension` implements the +The `SpringExtension` implements the link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`] extension API from JUnit Jupiter, which lets Spring provide dependency injection for test constructors, test methods, and test lifecycle callback methods. -Specifically, `SpringExtension` can inject dependencies from the test's +Specifically, the `SpringExtension` can inject dependencies from the test's `ApplicationContext` into test constructors and methods that are annotated with -`@BeforeAll`, `@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, -`@ParameterizedTest`, and others. +Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, +`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, +and others. + [[testcontext-junit-jupiter-di-constructor]] ==== Constructor Injection @@ -534,7 +536,7 @@ The _Spring TestContext Framework_ has supported the use of test-related annotat Framework 5.3 class-level test configuration annotations were not _inherited_ from enclosing classes like they are from superclasses. -Spring Framework 5.3 introduces first-class support for inheriting test class +Spring Framework 5.3 introduced 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 diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc index 2c2c749b88cb..c4117c5d4d33 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 @@ -29,7 +29,7 @@ by default, exactly in the following order: You can register `TestExecutionListener` implementations explicitly for a test class, its subclasses, and its nested classes by using the `@TestExecutionListeners` annotation. See xref:testing/annotations.adoc[annotation support] and the javadoc for -{api-spring-framework}/test/context/TestExecutionListeners.html[`@TestExecutionListeners`] +{spring-framework-api}/test/context/TestExecutionListeners.html[`@TestExecutionListeners`] for details and examples. .Switching to default `TestExecutionListener` implementations @@ -80,12 +80,12 @@ become cumbersome if a custom listener needs to be used across an entire test su issue is addressed through support for automatic discovery of default `TestExecutionListener` implementations through the `SpringFactoriesLoader` mechanism. -Specifically, the `spring-test` module declares all core default `TestExecutionListener` +For example, the `spring-test` module declares all core default `TestExecutionListener` implementations under the `org.springframework.test.context.TestExecutionListener` key in -its `META-INF/spring.factories` properties file. Third-party frameworks and developers -can contribute their own `TestExecutionListener` implementations to the list of default -listeners in the same manner through their own `META-INF/spring.factories` properties -file. +its {spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories` +properties file]. Third-party frameworks and developers can contribute their own +`TestExecutionListener` implementations to the list of default listeners in the same +manner through their own `spring.factories` files. [[testcontext-tel-config-ordering]] == Ordering `TestExecutionListener` Implementations @@ -207,4 +207,3 @@ Kotlin:: } ---- ====== - 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 3d609bf25e9c..f34ad15f9457 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc @@ -216,7 +216,7 @@ Support for `TestTransaction` is automatically available whenever the `TransactionalTestExecutionListener` is enabled. The following example demonstrates some of the features of `TestTransaction`. See the -javadoc for {api-spring-framework}/test/context/transaction/TestTransaction.html[`TestTransaction`] +javadoc for {spring-framework-api}/test/context/transaction/TestTransaction.html[`TestTransaction`] for further details. [tabs] @@ -295,14 +295,56 @@ behavior after your test runs (if the test was configured to commit the transact `TransactionalTestExecutionListener` supports the `@BeforeTransaction` and `@AfterTransaction` annotations for exactly such scenarios. You can annotate any `void` method in a test class or any `void` default method in a test interface with one of these -annotations, and the `TransactionalTestExecutionListener` ensures that your before -transaction method or after transaction method runs at the appropriate time. +annotations, and the `TransactionalTestExecutionListener` ensures that your +before-transaction method or after-transaction method runs at the appropriate time. -TIP: Any before methods (such as methods annotated with JUnit Jupiter's `@BeforeEach`) -and any after methods (such as methods annotated with JUnit Jupiter's `@AfterEach`) are -run within a transaction. In addition, methods annotated with `@BeforeTransaction` or -`@AfterTransaction` are not run for test methods that are not configured to run within a -transaction. +[NOTE] +==== +Generally speaking, `@BeforeTransaction` and `@AfterTransaction` methods must not accept +any arguments. + +However, as of Spring Framework 6.1, 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` +extension such as the `SpringExtension`. This means that JUnit-specific arguments like +`TestInfo` or beans from the test's `ApplicationContext` may be provided to +`@BeforeTransaction` and `@AfterTransaction` methods, as demonstrated in the following +example. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +@BeforeTransaction +void verifyInitialDatabaseState(@Autowired DataSource dataSource) { + // Use the DataSource to verify the initial state before a transaction is started +} +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +@BeforeTransaction +fun verifyInitialDatabaseState(@Autowired dataSource: DataSource) { + // Use the DataSource to verify the initial state before a transaction is started +} +---- +====== +==== + +[TIP] +==== +Any before methods (such as methods annotated with JUnit Jupiter's `@BeforeEach`) and any +after methods (such as methods annotated with JUnit Jupiter's `@AfterEach`) are run +within the test-managed transaction for a transactional test method. + +Similarly, methods annotated with `@BeforeTransaction` or `@AfterTransaction` are only +run for transactional test methods. +==== [[testcontext-tx-mgr-config]] == Configuring a Transaction Manager @@ -313,7 +355,7 @@ of `PlatformTransactionManager` within the test's `ApplicationContext`, you can qualifier by using `@Transactional("myTxMgr")` or `@Transactional(transactionManager = "myTxMgr")`, or `TransactionManagementConfigurer` can be implemented by an `@Configuration` class. Consult the -{api-spring-framework}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc +{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc for `TestContextTransactionUtils.retrieveTransactionManager()`] for details on the algorithm used to look up a transaction manager in the test's `ApplicationContext`. @@ -626,7 +668,7 @@ Kotlin:: ====== See -https://github.com/spring-projects/spring-framework/blob/main/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java[JpaEntityListenerTests] +{spring-framework-code}/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java[JpaEntityListenerTests] in the Spring Framework test suite for working examples using all JPA lifecycle callbacks. ===== diff --git a/framework-docs/modules/ROOT/pages/testing/unit.adoc b/framework-docs/modules/ROOT/pages/testing/unit.adoc index f137331a5138..d2ce8648b0e9 100644 --- a/framework-docs/modules/ROOT/pages/testing/unit.adoc +++ b/framework-docs/modules/ROOT/pages/testing/unit.adoc @@ -112,16 +112,16 @@ categories: The `org.springframework.test.util` package contains several general purpose utilities for use in unit and integration testing. -{api-spring-framework}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of +{spring-framework-api}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of AOP-related utility methods. You can use these methods to obtain a reference to the underlying target object hidden behind one or more Spring proxies. For example, if you have configured a bean as a dynamic mock by using a library such as EasyMock or Mockito, and the mock is wrapped in a Spring proxy, you may need direct access to the underlying mock to configure expectations on it and perform verifications. For Spring's core AOP -utilities, see {api-spring-framework}/aop/support/AopUtils.html[`AopUtils`] and -{api-spring-framework}/aop/framework/AopProxyUtils.html[`AopProxyUtils`]. +utilities, see {spring-framework-api}/aop/support/AopUtils.html[`AopUtils`] and +{spring-framework-api}/aop/framework/AopProxyUtils.html[`AopProxyUtils`]. -{api-spring-framework}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a +{spring-framework-api}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a collection of reflection-based utility methods. You can use these methods in testing scenarios where you need to change the value of a constant, set a non-`public` field, invoke a non-`public` setter method, or invoke a non-`public` configuration or lifecycle @@ -135,7 +135,7 @@ callback method when testing application code for use cases such as the followin * Use of annotations such as `@PostConstruct` and `@PreDestroy` for lifecycle callback methods. -{api-spring-framework}/test/util/TestSocketUtils.html[`TestSocketUtils`] is a simple +{spring-framework-api}/test/util/TestSocketUtils.html[`TestSocketUtils`] is a simple utility for finding available TCP ports on `localhost` for use in integration testing scenarios. @@ -155,7 +155,7 @@ server for the port it is currently using. === Spring MVC Testing Utilities The `org.springframework.test.web` package contains -{api-spring-framework}/test/web/ModelAndViewAssert.html[`ModelAndViewAssert`], which you +{spring-framework-api}/test/web/ModelAndViewAssert.html[`ModelAndViewAssert`], which you can use in combination with JUnit, TestNG, or any other testing framework for unit tests that deal with Spring MVC `ModelAndView` objects. diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 16a44856931c..7f0fa031ef80 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -7,9 +7,6 @@ but exposes a testing facade for verifying responses. `WebTestClient` can be use perform end-to-end HTTP tests. It can also be used to test Spring MVC and Spring WebFlux applications without a running server via mock server request and response objects. -TIP: Kotlin users: See xref:languages/kotlin/spring-projects-in.adoc#kotlin-webtestclient-issue[this section] -related to use of the `WebTestClient`. - @@ -51,7 +48,7 @@ Kotlin:: ====== For Spring MVC, use the following which delegates to the -{api-spring-framework}/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder] +{spring-framework-api}/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder] to load infrastructure equivalent to the xref:web/webmvc/mvc-config.adoc[WebMvc Java config], registers the given controller(s), and creates an instance of xref:testing/spring-mvc-test-framework.adoc[MockMvc] to handle requests: @@ -84,7 +81,7 @@ infrastructure and controller declarations and use it to handle requests via moc and response objects, without a running server. For WebFlux, use the following where the Spring `ApplicationContext` is passed to -{api-spring-framework}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder] +{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder] to create the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[WebHandler chain] to handle requests: @@ -130,7 +127,7 @@ Kotlin:: ====== For Spring MVC, use the following where the Spring `ApplicationContext` is passed to -{api-spring-framework}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup] +{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 requests: @@ -342,6 +339,19 @@ Java:: spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) ); ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + 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: @@ -442,7 +452,7 @@ Kotlin:: TIP: When you need to decode to a target type with generics, look for the overloaded methods that accept -{api-spring-framework}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`] +{spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`] instead of `Class`. @@ -675,13 +685,13 @@ Kotlin:: val result = client.get().uri("/persons/1") .exchange() .expectStatus().isOk() - .expectBody(Person.class) - .returnResult(); + .expectBody() + .returnResult() // For a response without a body val result = client.get().uri("/path") .exchange() - .expectBody().isEmpty(); + .expectBody().isEmpty() ---- ====== @@ -707,4 +717,3 @@ Kotlin:: .andExpect(model().attribute("string", "a string value")); ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/web-reactive.adoc b/framework-docs/modules/ROOT/pages/web-reactive.adoc index b7ef832ffa33..378d65ef5911 100644 --- a/framework-docs/modules/ROOT/pages/web-reactive.adoc +++ b/framework-docs/modules/ROOT/pages/web-reactive.adoc @@ -2,10 +2,11 @@ = Web on Reactive Stack This part of the documentation covers support for reactive-stack web applications built -on a https://www.reactive-streams.org/[Reactive Streams] API to run on non-blocking +on a {reactive-streams-site}/[Reactive Streams] API to run on non-blocking servers, such as Netty, Undertow, and Servlet containers. Individual chapters cover the xref:web/webflux.adoc#webflux[Spring WebFlux] framework, -the reactive xref:web/webflux-webclient.adoc[`WebClient`], support for xref:web-reactive.adoc#webflux-test[testing], -and xref:web-reactive.adoc#webflux-reactive-libraries[reactive libraries]. For Servlet-stack web applications, -see xref:web.adoc[Web on Servlet Stack]. +the reactive xref:web/webflux-webclient.adoc[`WebClient`], +support for xref:web/webflux-test.adoc[testing], +and xref:web/webflux-reactive-libraries.adoc[reactive libraries]. For Servlet-stack web +applications, see xref:web.adoc[Web on Servlet Stack]. diff --git a/framework-docs/modules/ROOT/pages/web.adoc b/framework-docs/modules/ROOT/pages/web.adoc index 76ebcfc90689..e8a18f927c88 100644 --- a/framework-docs/modules/ROOT/pages/web.adoc +++ b/framework-docs/modules/ROOT/pages/web.adoc @@ -5,9 +5,5 @@ This part of the documentation covers support for Servlet-stack web applications built on the Servlet API and deployed to Servlet containers. Individual chapters include xref:web/webmvc.adoc#mvc[Spring MVC], xref:web/webmvc-view.adoc[View Technologies], xref:web/webmvc-cors.adoc[CORS Support], and xref:web/websocket.adoc[WebSocket Support]. -For reactive-stack web applications, see xref:testing/unit.adoc#mock-objects-web-reactive[Web on Reactive Stack]. - - - - +For reactive-stack web applications, see xref:web-reactive.adoc[Web on Reactive Stack]. diff --git a/framework-docs/modules/ROOT/pages/web/integration.adoc b/framework-docs/modules/ROOT/pages/web/integration.adoc index 6d362d916ae7..55276ff2bcb2 100644 --- a/framework-docs/modules/ROOT/pages/web/integration.adoc +++ b/framework-docs/modules/ROOT/pages/web/integration.adoc @@ -35,7 +35,7 @@ context"). This section details how you can configure a Spring container (a `WebApplicationContext`) that contains all of the 'business beans' in your application. Moving on to specifics, all you need to do is declare a -{api-spring-framework}/web/context/ContextLoaderListener.html[`ContextLoaderListener`] +{spring-framework-api}/web/context/ContextLoaderListener.html[`ContextLoaderListener`] in the standard Jakarta EE servlet `web.xml` file of your web application and add a `contextConfigLocation` `` section (in the same file) that defines which set of Spring XML configuration files to load. @@ -62,7 +62,7 @@ Further consider the following `` configuration: If you do not specify the `contextConfigLocation` context parameter, the `ContextLoaderListener` looks for a file called `/WEB-INF/applicationContext.xml` to load. Once the context files are loaded, Spring creates a -{api-spring-framework}/web/context/WebApplicationContext.html[`WebApplicationContext`] +{spring-framework-api}/web/context/WebApplicationContext.html[`WebApplicationContext`] object based on the bean definitions and stores it in the `ServletContext` of the web application. @@ -78,7 +78,7 @@ The following example shows how to get the `WebApplicationContext`: ---- The -{api-spring-framework}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`] +{spring-framework-api}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`] class is for convenience, so you need not remember the name of the `ServletContext` attribute. Its `getWebApplicationContext()` method returns `null` if an object does not exist under the `WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE` @@ -142,7 +142,7 @@ Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF A custom `ELResolver` works well when mapping your properties to beans in `faces-config.xml`, but, at times, you may need to explicitly grab a bean. -The {api-spring-framework}/web/jsf/FacesContextUtils.html[`FacesContextUtils`] +The {spring-framework-api}/web/jsf/FacesContextUtils.html[`FacesContextUtils`] class makes this easy. It is similar to `WebApplicationContextUtils`, except that it takes a `FacesContext` parameter rather than a `ServletContext` parameter. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc index 845976b8e02c..4de277efa7ea 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc @@ -47,7 +47,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r and, consequently, browsers reject them. Each `HandlerMapping` can be -{api-spring-framework}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] +{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications use the WebFlux Java configuration to declare such mappings, which results in a single, global map passed to all `HandlerMapping` implementations. @@ -60,7 +60,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement The rules for combining global and local configuration are generally additive -- for example, all global and all local origins. For those attributes where only a single value can be accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See -{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] +{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] for more details. [TIP] @@ -108,7 +108,7 @@ a finite set of values instead to provide a higher level of security. == `@CrossOrigin` [.small]#xref:web/webmvc-cors.adoc#mvc-cors-controller[See equivalent in the Servlet stack]# -The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] +The {spring-framework-api}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] annotation enables cross-origin requests on annotated controller methods, as the following example shows: @@ -363,7 +363,7 @@ Kotlin:: [.small]#xref:web/webmvc-cors.adoc#mvc-cors-filter[See equivalent in the Servlet stack]# You can apply CORS support through the built-in -{api-spring-framework}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a +{spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a good fit with <>. NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring diff --git a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc index 3e3cb23c9f86..5f03e5a131ea 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc @@ -122,7 +122,7 @@ Most applications can run through the WebFlux Java configuration, see xref:web/w `ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly access to the HTTP request and response. -Both request and response provide https://www.reactive-streams.org[Reactive Streams] back pressure +Both request and response provide {reactive-streams-site}[Reactive Streams] back pressure against the body streams. The request body is represented with a Reactor `Flux` or `Mono`. The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`. @@ -350,7 +350,7 @@ ServerResponse.created(location).build() ====== Depending on the codec used, it is possible to pass hint parameters to customize how the -body is serialized or deserialized. For example, to specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON view]: +body is serialized or deserialized. For example, to specify a {baeldung-blog}/jackson-json-view-annotation[Jackson JSON view]: [tabs] ====== @@ -782,6 +782,73 @@ Kotlin:: ====== +[[webflux-fn-serving-resources]] +== Serving Resources + +WebFlux.fn provides built-in support for serving resources. + +NOTE: In addition to the capabilities described below, it is possible to implement even more flexible resource handling thanks to +{spring-framework-api}++/web/reactive/function/server/RouterFunctions.html#resources(java.util.function.Function)++[`RouterFunctions#resource(java.util.function.Function)`]. + +[[webflux-fn-resource]] +=== Redirecting to a resource + +It is possible to redirect requests matching a specified predicate to a resource. This can be useful, for example, +for handling redirects in Single Page Applications. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ClassPathResource index = new ClassPathResource("static/index.html"); + List extensions = Arrays.asList("js", "css", "ico", "png", "jpg", "gif"); + RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate(); + RouterFunction redirectToIndex = route() + .resource(spaPredicate, index) + .build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + 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)) + resource(spaPredicate, index) + } +---- +====== + +[[webflux-fn-resources]] +=== Serving resources from a root location + +It is also possible to route requests that match a given pattern to resources relative to a given root location. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + Resource location = new FileSystemResource("public-resources/"); + RouterFunction resources = RouterFunctions.resources("/resources/**", location); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val location = FileSystemResource("public-resources/") + val resources = router { resources("/resources/**", location) } +---- +====== + + [[webflux-fn-running]] == Running a Server [.small]#xref:web/webmvc-functional.adoc#webmvc-fn-running[See equivalent in the Servlet stack]# diff --git a/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc b/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc index 45ac4fcd581d..0e7253dae526 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc @@ -4,26 +4,17 @@ `spring-webflux` depends on `reactor-core` and uses it internally to compose asynchronous logic and to provide Reactive Streams support. Generally, WebFlux APIs return `Flux` or `Mono` (since those are used internally) and leniently accept any Reactive Streams -`Publisher` implementation as input. The use of `Flux` versus `Mono` is important, because -it helps to express cardinality -- for example, whether a single or multiple asynchronous -values are expected, and that can be essential for making decisions (for example, when -encoding or decoding HTTP messages). +`Publisher` implementation as input. +When a `Publisher` is provided, it can be treated only as a stream with unknown semantics (0..N). +If, however, the semantics are known, you should wrap it with `Flux` or `Mono.from(Publisher)` instead +of passing the raw `Publisher`. +The use of `Flux` versus `Mono` is important, because it helps to express cardinality -- +for example, whether a single or multiple asynchronous values are expected, +and that can be essential for making decisions (for example, when encoding or decoding HTTP messages). For annotated controllers, WebFlux transparently adapts to the reactive library chosen by the application. This is done with the help of the -{api-spring-framework}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`], which +{spring-framework-api}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`], which provides pluggable support for reactive library and other asynchronous types. The registry has built-in support for RxJava 3, Kotlin coroutines and SmallRye Mutiny, but you can register others, too. - -For functional APIs (such as <>, the `WebClient`, and others), the general rules -for WebFlux APIs apply -- `Flux` and `Mono` as return values and a Reactive Streams -`Publisher` as input. When a `Publisher`, whether custom or from another reactive library, -is provided, it can be treated only as a stream with unknown semantics (0..N). If, however, -the semantics are known, you can wrap it with `Flux` or `Mono.from(Publisher)` instead -of passing the raw `Publisher`. - -For example, given a `Publisher` that is not a `Mono`, the Jackson JSON message writer -expects multiple values. If the media type implies an infinite stream (for example, -`application/json+stream`), values are written and flushed individually. Otherwise, -values are buffered into a list and rendered as a JSON array. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc index 491a064f3ffd..288fc1a38c52 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc @@ -209,7 +209,7 @@ sections of the Spring MVC documentation. The Spring Framework has a built-in integration for using Spring WebFlux with any templating library that can run on top of the -https://www.jcp.org/en/jsr/detail?id=223[JSR-223] Java scripting engine. +{JSR}223[JSR-223] Java scripting engine. The following table shows the templating libraries that we have tested on different script engines: [%header] @@ -221,7 +221,7 @@ The following table shows the templating libraries that we have tested on differ |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.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython] -|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |https://kotlinlang.org/[Kotlin] +|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== TIP: The basic rule for integrating any other script engine is that it must implement the @@ -312,7 +312,7 @@ The `render` function is called with the following parameters: * `String template`: The template content * `Map model`: The view model * `RenderingContext renderingContext`: The - {api-spring-framework}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] + {spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] that gives access to the application context, the locale, the template loader, and the URL (since 5.0) @@ -404,8 +404,8 @@ The following example shows how compile a template: ---- Check out the Spring Framework unit tests, -{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and -{spring-framework-main-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources], +{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and +{spring-framework-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources], for more configuration examples. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc index effa703ab605..448ff3db92d8 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc @@ -12,8 +12,8 @@ decode request and response content on the server side. `WebClient` needs an HTTP client library to perform requests with. There is built-in support for the following: -* https://github.com/reactor/reactor-netty[Reactor Netty] -* https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html[JDK HttpClient] +* {reactor-github-org}/reactor-netty[Reactor Netty] +* {java-api}/java.net.http/java/net/http/HttpClient.html[JDK HttpClient] * https://github.com/jetty-project/jetty-reactive-httpclient[Jetty Reactive HttpClient] * https://hc.apache.org/index.html[Apache HttpComponents] * Others can be plugged via `ClientHttpConnector`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc index 4fd49bf8045a..c1bd622687c0 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 @@ -148,5 +148,75 @@ Kotlin:: ---- ====== +The example below demonstrates how to use the `ExchangeFilterFunction` interface to create +a custom filter class that helps with computing a `Content-Length` header for `PUT` and `POST` +`multipart/form-data` requests using buffering. +[tabs] +====== +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)); + }); + } + } +} +---- +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 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..75e0000bcc24 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 @@ -97,8 +97,8 @@ Java:: 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); ---- @@ -109,8 +109,8 @@ Kotlin:: 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-testing.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc index 8b1393cc52b6..febbb5498272 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc @@ -5,7 +5,7 @@ To test code that uses the `WebClient`, you can use a mock web server, such as the https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example of its use, check out -{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] +{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] in the Spring Framework test suite or the https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`] sample in the OkHttp repository. diff --git a/framework-docs/modules/ROOT/pages/web/webflux.adoc b/framework-docs/modules/ROOT/pages/web/webflux.adoc index a9487c93739b..cbf487481cb8 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux.adoc @@ -7,12 +7,12 @@ The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports -https://www.reactive-streams.org/[Reactive Streams] back pressure, and runs on such servers as +{reactive-streams-site}/[Reactive Streams] back pressure, and runs on such servers as Netty, Undertow, and Servlet containers. Both web frameworks mirror the names of their source modules -({spring-framework-main-code}/spring-webmvc[spring-webmvc] and -{spring-framework-main-code}/spring-webflux[spring-webflux]) and co-exist side by side in the +({spring-framework-code}/spring-webmvc[spring-webmvc] and +{spring-framework-code}/spring-webflux[spring-webflux]) and co-exist side by side in the Spring Framework. Each module is optional. Applications can use one or the other module or, in some cases, both -- for example, Spring MVC controllers with the reactive `WebClient`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc index 188d58a1ad72..39c5ac92357c 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc @@ -5,14 +5,14 @@ A common requirement for REST services is to include details in the body of error responses. The Spring Framework supports the "Problem Details for HTTP APIs" -specification, https://www.rfc-editor.org/rfc/rfc7807.html[RFC 7807]. +specification, {rfc-site}/rfc9457.html[RFC 9457]. The following are the main abstractions for this support: -- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container +- `ProblemDetail` -- representation for an RFC 9457 problem detail; a simple container for both standard fields defined in the spec, and for non-standard ones. - `ErrorResponse` -- contract to expose HTTP error response details including HTTP -status, response headers, and a body in the format of RFC 7807; this allows exceptions to +status, response headers, and a body in the format of RFC 9457; this allows exceptions to encapsulate and expose the details of how they map to an HTTP response. All Spring WebFlux exceptions implement this. - `ErrorResponseException` -- basic `ErrorResponse` implementation that others @@ -28,7 +28,7 @@ and any `ErrorResponseException`, and renders an error response with a body. [.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-render[See equivalent in the Servlet stack]# You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from -any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: +any `@RequestMapping` method to render an RFC 9457 response. This is processed as follows: - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not @@ -37,7 +37,7 @@ already set. "application/problem+json" over "application/json" when rendering a `ProblemDetail`, and also falls back on it if no compatible media type is found. -To enable RFC 7807 responses for Spring WebFlux exceptions and for any +To enable RFC 9457 responses for Spring WebFlux exceptions and for any `ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an xref:web/webflux/controller/ann-advice.adoc[@ControllerAdvice] in Spring configuration. The handler has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which @@ -50,7 +50,7 @@ use a protected method to map any exception to a `ProblemDetail`. == Non-Standard Fields [.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-non-standard[See equivalent in the Servlet stack]# -You can extend an RFC 7807 response with non-standard fields in one of two ways. +You can extend an RFC 9457 response with non-standard fields in one of two ways. One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this @@ -67,52 +67,44 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an [[webflux-ann-rest-exceptions-i18n]] -== Internationalization +== Customization and i18n [.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-i18n[See equivalent in the Servlet stack]# -It is a common requirement to internationalize error response details, and good practice -to customize the problem details for Spring WebFlux exceptions. This is supported as follows: +It is a common requirement to customize and internationalize error response details. +It is also good practice to customize the problem details for Spring WebFlux exceptions +to avoid revealing implementation details. This section describes the support for that. -- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field -through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]. -The actual message code value is parameterized with placeholders, e.g. -`+"HTTP method {0} not supported"+` to be expanded from the arguments. -- Each `ErrorResponse` also exposes a message code to resolve the "title" field. -- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the -"detail" and the "title" fields. +An `ErrorResponse` exposes message codes for "type", "title", and "detail", as well as +message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves +these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource] +and updates the corresponding `ProblemDetail` fields accordingly. -By default, the message code for the "detail" field is "problemDetail." + the fully -qualified exception class name. Some exceptions may expose additional message codes in -which case a suffix is added to the default message code. The table below lists message -arguments and codes for Spring WebFlux exceptions: +The default strategy for message codes follows the pattern: + +`problemDetail.[type|title|detail].[fully qualified exception class name]` + +An `ErrorResponse` may expose more than one message code, typically adding a suffix +to the default message code. The table below lists message codes, and arguments for +Spring WebFlux exceptions: [[webflux-ann-rest-exceptions-codes]] [cols="1,1,2", options="header"] |=== | Exception | Message Code | Message Code Arguments -| `UnsupportedMediaTypeStatusException` +| `HandlerMethodValidationException` | (default) -| `+{0}+` the media type that is not supported, `+{1}+` list of supported media types +| `+{0}+` list all validation errors. +Message codes and arguments for each error are also resolved via `MessageSource`. -| `UnsupportedMediaTypeStatusException` -| (default) + ".parseError" -| +| `MethodNotAllowedException` +| (default) +| `+{0}+` the current HTTP method, `+{1}+` the list of supported HTTP methods | `MissingRequestValueException` | (default) | `+{0}+` a label for the value (e.g. "request header", "cookie value", ...), `+{1}+` the value name -| `UnsatisfiedRequestParameterException` -| (default) -| `+{0}+` the list of parameter conditions - -| `WebExchangeBindException` -| (default) -| `+{0}+` the list of global errors, `+{1}+` the list of field errors. -Message codes and arguments for each error within the `BindingResult` are also resolved -via `MessageSource`. - | `NotAcceptableStatusException` | (default) | `+{0}+` list of supported media types @@ -125,14 +117,32 @@ via `MessageSource`. | (default) | `+{0}+` the failure reason provided to the class constructor -| `MethodNotAllowedException` +| `UnsupportedMediaTypeStatusException` | (default) -| `+{0}+` the current HTTP method, `+{1}+` the list of supported HTTP methods +| `+{0}+` the media type that is not supported, `+{1}+` list of supported media types + +| `UnsupportedMediaTypeStatusException` +| (default) + ".parseError" +| + +| `UnsatisfiedRequestParameterException` +| (default) +| `+{0}+` the list of parameter conditions + +| `WebExchangeBindException` +| (default) +| `+{0}+` the list of global errors, `+{1}+` the list of field errors. +Message codes and arguments for each error are also resolved via `MessageSource`. |=== -By default, the message code for the "title" field is "problemDetail.title." + the fully -qualified exception class name. +NOTE: Unlike other exceptions, the message arguments for +`WebExchangeBindException` 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 +xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method-i18n[Customizing Validation Errors] +for more details. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc b/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc index 5da8d201aedf..a01e743b5edf 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc @@ -19,14 +19,14 @@ This section describes the HTTP caching related options available in Spring WebF == `CacheControl` [.small]#xref:web/webmvc/mvc-caching.adoc#mvc-caching-cachecontrol[See equivalent in the Servlet stack]# -{api-spring-framework}/http/CacheControl.html[`CacheControl`] provides support for +{spring-framework-api}/http/CacheControl.html[`CacheControl`] provides support for configuring settings related to the `Cache-Control` header and is accepted as an argument in a number of places: * xref:web/webflux/caching.adoc#webflux-caching-etag-lastmodified[Controllers] * xref:web/webflux/caching.adoc#webflux-caching-static-resources[Static Resources] -While https://tools.ietf.org/html/rfc7234#section-5.2.2[RFC 7234] describes all possible +While {rfc-site}/rfc7234#section-5.2.2[RFC 7234] describes all possible directives for the `Cache-Control` response header, the `CacheControl` type takes a use case-oriented approach that focuses on the common scenarios, as the following example shows: diff --git a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc index 10e87907b6dc..86d3ed91c2bd 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc @@ -338,7 +338,7 @@ Kotlin:: class WebConfig : WebFluxConfigurer { override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { - // ... + configurer.defaultCodecs().maxInMemorySize(512 * 1024) } } ---- @@ -348,18 +348,18 @@ Kotlin:: more readers and writers, customize the default ones, or replace the default ones completely. For Jackson JSON and XML, consider using -{api-spring-framework}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`], +{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`], which customizes Jackson's default properties with the following ones: -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. It also automatically registers the following well-known modules if they are detected on the classpath: -* https://github.com/FasterXML/jackson-datatype-joda[`jackson-datatype-joda`]: Support for Joda-Time types. -* https://github.com/FasterXML/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types. -* https://github.com/FasterXML/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`. -* https://github.com/FasterXML/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes. +* {jackson-github-org}/jackson-datatype-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. @@ -549,7 +549,7 @@ See xref:web/webflux-view.adoc[View Technologies] for more on the view technolog [.small]#xref:web/webmvc/mvc-config/static-resources.adoc[See equivalent in the Servlet stack]# This option provides a convenient way to serve static resources from a list of -{api-spring-framework}/core/io/Resource.html[`Resource`]-based locations. +{spring-framework-api}/core/io/Resource.html[`Resource`]-based locations. In the next example, given a request that starts with `/resources`, the relative path is used to find and serve static resources relative to `/static` on the classpath. Resources @@ -598,8 +598,8 @@ Kotlin:: See also xref:web/webflux/caching.adoc#webflux-caching-static-resources[HTTP caching support for static resources]. The resource handler also supports a chain of -{api-spring-framework}/web/reactive/resource/ResourceResolver.html[`ResourceResolver`] implementations and -{api-spring-framework}/web/reactive/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, +{spring-framework-api}/web/reactive/resource/ResourceResolver.html[`ResourceResolver`] implementations and +{spring-framework-api}/web/reactive/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, which can be used to create a toolchain for working with optimized resources. You can use the `VersionResourceResolver` for versioned resource URLs based on an MD5 hash @@ -685,9 +685,37 @@ for fine-grained control, e.g. last-modified behavior and optimized resource res [.small]#xref:web/webmvc/mvc-config/path-matching.adoc[See equivalent in the Servlet stack]# You can customize options related to path matching. For details on the individual options, see the -{api-spring-framework}/web/reactive/config/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. +{spring-framework-api}/web/reactive/config/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. The following example shows how to use `PathMatchConfigurer`: +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-blocking-execution]] +== Blocking Execution + +The WebFlux Java config allows you to customize blocking execution in WebFlux. + +You can have blocking controller methods called on a separate thread by providing +an `AsyncTaskExecutor` such as the +{spring-framework-api}/core/task/VirtualThreadTaskExecutor.html[`VirtualThreadTaskExecutor`] +as follows: + [tabs] ====== Java:: @@ -699,10 +727,9 @@ Java:: public class WebConfig implements WebFluxConfigurer { @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - configurer - .setUseCaseSensitiveMatch(true) - .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); + public void configureBlockingExecution(BlockingExecutionConfigurer configurer) { + AsyncTaskExecutor executor = ... + configurer.setExecutor(executor); } } ---- @@ -716,27 +743,18 @@ Kotlin:: class WebConfig : WebFluxConfigurer { @Override - fun configurePathMatch(configurer: PathMatchConfigurer) { - configurer - .setUseCaseSensitiveMatch(true) - .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + fun configureBlockingExecution(configurer: BlockingExecutionConfigurer) { + val executor = ... + configurer.setExecutor(executor) } } ---- ====== -[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. +By default, controller methods whose return type is not recognized by the configured +`ReactiveAdapterRegistry` are considered blocking, but you can set a custom controller +method predicate via `BlockingExecutionConfigurer`. -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. -==== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-advice.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-advice.adoc index 362425369283..cf77c0ca8409 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 @@ -64,6 +64,6 @@ Kotlin:: The selectors in the preceding example are evaluated at runtime and may negatively impact performance if used extensively. See the -{api-spring-framework}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] +{spring-framework-api}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] javadoc for more details. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-initbinder.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-initbinder.adoc index 320d56b3caa1..3893881ea42d 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 @@ -3,23 +3,21 @@ [.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc[See equivalent in the Servlet stack]# -`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods, to -initialize instances of `WebDataBinder`. Those, in turn, are used to: +`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to +initialize `WebDataBinder` instances that in turn can: -* Bind request parameters (that is, form data or query) to a model object. -* Convert `String`-based request values (such as request parameters, path variables, -headers, cookies, and others) to the target type of controller method arguments. -* Format model object values as `String` values when rendering HTML forms. +* Bind request parameters to a model object. +* Convert request values from string to object property types. +* Format model object properties as strings when rendering HTML forms. -`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or -Spring `Converter` and `Formatter` components. In addition, you can use the -xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux Java configuration] to register `Converter` and -`Formatter` types in a globally shared `FormattingConversionService`. +In an `@Controller`, `DataBinder` customizations apply locally within the controller, +or even to a specific model attribute referenced by name through the annotation. +In an `@ControllerAdvice` customizations can apply to all or a subset of controllers. -`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods -do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared -with a `WebDataBinder` argument, for registrations, and a `void` return value. -The following example uses the `@InitBinder` annotation: +You can register `PropertyEditor`, `Converter`, and `Formatter` components in the +`DataBinder` for type conversion. Alternatively, you can use the +xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux config] to register +`Converter` and `Formatter` components in a globally shared `FormattingConversionService`. -- [tabs] @@ -112,4 +110,5 @@ Kotlin:: == 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[] diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc index 87cb4522b2aa..37d297afe3ba 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc @@ -77,7 +77,7 @@ and others) and is equivalent to `required=false`. | For access to a part in a `multipart/form-data` request. Supports reactive types. See xref:web/webflux/controller/ann-methods/multipart-forms.adoc[Multipart Content] and xref:web/webflux/reactive-spring.adoc#webflux-multipart[Multipart Data]. -| `java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`. +| `java.util.Map` or `org.springframework.ui.Model` | For access to the model that is used in HTML controllers and is exposed to templates as part of view rendering. @@ -89,9 +89,9 @@ and others) and is equivalent to `required=false`. Note that use of `@ModelAttribute` is optional -- for example, to set its attributes. See "`Any other argument`" later in this table. -| `Errors`, `BindingResult` +| `Errors` or `BindingResult` | For access to errors from validation and data binding for a command object, i.e. a - `@ModelAttribute` argument. An `Errors`, or `BindingResult` argument must be declared + `@ModelAttribute` argument. An `Errors` or `BindingResult` argument must be declared immediately after the validated method argument. | `SessionStatus` + class-level `@SessionAttributes` @@ -114,7 +114,7 @@ and others) and is equivalent to `required=false`. | Any other argument | If a method argument is not matched to any of the above, it is, by default, resolved as a `@RequestParam` if it is a simple type, as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], or as a `@ModelAttribute`, otherwise. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc index fe3db7c7d560..ba07fff65a59 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc @@ -8,7 +8,7 @@ Spring offers support for the Jackson JSON library. [.small]#xref:web/webmvc/mvc-controller/ann-methods/jackson.adoc[See equivalent in the Servlet stack]# Spring WebFlux provides built-in support for -https://www.baeldung.com/jackson-json-view-annotation[Jackson's Serialization Views], +{baeldung-blog}/jackson-json-view-annotation[Jackson's Serialization Views], which allows rendering only a subset of all fields in an `Object`. To use it with `@ResponseBody` or `ResponseEntity` controller methods, you can use Jackson's `@JsonView` annotation to activate a serialization view class, as the following example shows: @@ -81,7 +81,7 @@ Kotlin:: ---- ====== -NOTE: `@JsonView` allows an array of view classes but you can only specify only one per +NOTE: `@JsonView` allows an array of view classes but you can specify only one per controller method. Use a composite interface if you need to activate multiple views. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc index b5a5bd9753c5..02d4555997dd 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc @@ -3,7 +3,7 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc[See equivalent in the Servlet stack]# -https://tools.ietf.org/html/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in +{rfc-site}/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in path segments. In Spring WebFlux, we refer to those as "`matrix variables`" based on an https://www.w3.org/DesignIssues/MatrixURIs.html["`old post`"] by Tim Berners-Lee, but they can be also be referred to as URI path parameters. 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 347a025545a9..45977c0b9e65 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,11 +3,8 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Servlet stack]# -You can use the `@ModelAttribute` annotation on a method argument to access an attribute from the -model or have it instantiated if not present. The model attribute is also overlaid with -the values of query parameters and form fields whose names match to field names. This is -referred to as data binding, and it saves you from having to deal with parsing and -converting individual query parameters and form fields. The following example binds an instance of `Pet`: +The `@ModelAttribute` method parameter annotation binds request parameters onto a model +object. For example: [tabs] ====== @@ -18,7 +15,7 @@ Java:: @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute Pet pet) { } // <1> ---- -<1> Bind an instance of `Pet`. +<1> Bind to an instance of `Pet`. Kotlin:: + @@ -27,28 +24,31 @@ Kotlin:: @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@ModelAttribute pet: Pet): String { } // <1> ---- -<1> Bind an instance of `Pet`. +<1> Bind to an instance of `Pet`. ====== -The `Pet` instance in the preceding example is resolved as follows: - -* From the model if already added through xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`]. -* From the HTTP session through xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`]. -* From the invocation of a default constructor. -* From the invocation of a "`primary constructor`" with arguments that match query -parameters or form fields. Argument names are determined through JavaBeans -`@ConstructorProperties` or through runtime-retained parameter names in the bytecode. - -After the model attribute instance is obtained, data binding is applied. The -`WebExchangeDataBinder` class matches names of query parameters and form fields to field -names on the target `Object`. Matching fields are populated after type conversion is applied -where necessary. For more on data binding (and validation), see -xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see -xref:web/webflux/controller/ann-initbinder.adoc[`DataBinder`]. - -Data binding can result in errors. By default, a `WebExchangeBindException` is raised, but, -to check for such errors in the controller method, you can add a `BindingResult` argument -immediately next to the `@ModelAttribute`, as the following example shows: +The `Pet` instance may be: + +* Accessed from the model where it could have been added by a + xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`]. +* Accessed from the HTTP session if the model attribute was listed in + the class-level xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`]. +* Instantiated through a default constructor. +* Instantiated through a "`primary constructor`" with arguments that match to Servlet +request parameters. Argument names are determined through runtime-retained parameter +names in the bytecode. + +By default, both constructor and property +xref:core/validation/beans-beans.adoc#beans-binding[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_ +patterns should be set to limit which properties can be set. For further details on this +and example configuration, see +xref:web/webflux/controller/ann-initbinder.adoc#webflux-ann-initbinder-model-design[model design]. + +When using constructor binding, you can customize request parameter names through an +`@BindParam` annotation. For example: [tabs] ====== @@ -56,35 +56,34 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { <1> - if (result.hasErrors()) { - return "petForm"; + class Account { + + private final String firstName; + + public Account(@BindParam("first-name") String firstName) { + this.firstName = firstName; } - // ... } ---- -<1> Adding a `BindingResult`. - Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> - if (result.hasErrors()) { - return "petForm" - } - // ... - } + class Account(@BindParam("first-name") val firstName: String) ---- -<1> Adding a `BindingResult`. ====== -You can automatically apply validation after data binding by adding the -`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see also -xref:core/validation/beanvalidation.adoc[Bean Validation] and -xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example uses the `@Valid` annotation: +NOTE: The `@BindParam` may also be placed on the fields that correspond to constructor +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`. +You can declare a `@ModelAttribute` argument with or without a reactive type wrapper, and +it will be resolved accordingly to the actual value. + +If data binding results in errors, by default a `WebExchangeBindException` is raised, +but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute` +in order to handle such errors in the controller method. For example: [tabs] ====== @@ -93,37 +92,33 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1> + public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { <1> if (result.hasErrors()) { return "petForm"; } // ... } ---- -<1> Using `@Valid` on a model attribute argument. +<1> Adding a `BindingResult`. Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> + fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> if (result.hasErrors()) { return "petForm" } // ... } ---- -<1> Using `@Valid` on a model attribute argument. +<1> Adding a `BindingResult`. ====== -Spring WebFlux, unlike Spring MVC, supports reactive types in the model -- for example, -`Mono` or `io.reactivex.Single`. You can declare a `@ModelAttribute` argument -with or without a reactive type wrapper, and it will be resolved accordingly, -to the actual value if necessary. However, note that, to use a `BindingResult` -argument, you must declare the `@ModelAttribute` argument before it without a reactive -type wrapper, as shown earlier. Alternatively, you can handle any errors through the -reactive type, as the following example shows: +To use a `BindingResult` argument, you must declare the `@ModelAttribute` argument before +it without a reactive type wrapper. If you want to use the reactive, you can handle errors +directly through it. For example: [tabs] ====== @@ -160,10 +155,52 @@ Kotlin:: ---- ====== -Note that use of `@ModelAttribute` is optional -- for example, to set its attributes. -By default, any argument that is not a simple value type (as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) -and is not resolved by any other argument resolver is treated as if it were annotated -with `@ModelAttribute`. +You can automatically apply validation after data binding by adding the +`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see +xref:core/validation/beanvalidation.adoc[Bean Validation] and +xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1> + if (result.hasErrors()) { + return "petForm"; + } + // ... + } +---- +<1> Using `@Valid` on a model attribute argument. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> + if (result.hasErrors()) { + return "petForm" + } + // ... + } +---- +<1> Using `@Valid` on a model attribute argument. +====== + +If method validation applies because other parameters have `@Constraint` annotations, +then `HandlerMethodValidationException` would be raised instead. See the section on +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] +_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`. +WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute` +support described above does not allow proper ahead-of-time inference of related data +binding reflection hints. As a consequence, it is recommended to explicitly annotate +method parameters with `@ModelAttribute` for use in a GraalVM native image. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc index 3ded73de6d63..462bf3eccb1f 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 @@ -176,6 +176,10 @@ Kotlin:: ====== -- +If method validation applies because other parameters have `@Constraint` annotations, +then `HandlerMethodValidationException` is raised instead. See the section on +xref:web/webflux/controller/ann-validation.adoc[Validation]. + To access all multipart data as a `MultiValueMap`, you can use `@RequestBody`, as the following example shows: @@ -306,5 +310,3 @@ file upload. 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/requestbody.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestbody.adoc index 8190c2811251..b4b78afd28db 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 @@ -89,4 +89,33 @@ Kotlin:: ---- ====== +You can also declare an `Errors` parameter for access to validation errors, but in +that case the request body must not be a `Mono`, and will be resolved first: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @PostMapping("/accounts") + public void handle(@Valid @RequestBody Account account, Errors errors) { + // use one of the onError* operators... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @PostMapping("/accounts") + fun handle(@Valid @RequestBody account: Mono) { + // ... + } +---- +====== + +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/requestparam.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestparam.adoc index 2752035758fd..372c4bfaa6be 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 @@ -74,7 +74,7 @@ When a `@RequestParam` annotation is declared on a `Map` or Note that use of `@RequestParam` is optional -- for example, to set its attributes. By default, any argument that is a simple value type (as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) and is not resolved by any other argument resolver is treated as if it were annotated with `@RequestParam`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc index 7752ee485377..7c06be75bf6c 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc @@ -4,9 +4,20 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods/return-types.adoc[See equivalent in the Servlet stack]# The following table shows the supported controller method return values. Note that reactive -types from libraries such as Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other] are +types from libraries such as Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other] are generally supported for all return values. +For return types like `Flux`, when multiple values are expected, elements are streamed as they come +and are not buffered. This is the default behavior, as keeping a potentially large amount of elements in memory +is not efficient. If the media type implies an infinite stream (for example, +`application/json+stream`), values are written and flushed individually. Otherwise, +values are written individually and the flushing happens separately. + +NOTE: If an error happens while an element is encoded to JSON, the response might have been written to and committed already +and it is impossible at that point to render a proper error response. +In some cases, applications can choose to trade memory efficiency for better handling such errors by buffering elements and encoding them all at once. +Controllers can then return a `Flux>`; Reactor provides a dedicated operator for that, `Flux#collectList()`. + [cols="1,2", options="header"] |=== | Controller method return value | Description @@ -24,11 +35,11 @@ generally supported for all return values. | For returning a response with headers and no body. | `ErrorResponse` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] | `ProblemDetail` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] | `String` @@ -75,7 +86,7 @@ generally supported for all return values. | Other return values | If a return value remains unresolved in any other way, it is treated as a model attribute, unless it is a simple type as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], in which case it remains unresolved. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc index bf8d7afa04e9..fab54b23073a 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 @@ -75,7 +75,7 @@ Kotlin:: ====== NOTE: When a name is not explicitly specified, a default name is chosen based on the type, -as explained in the javadoc for {api-spring-framework}/core/Conventions.html[`Conventions`]. +as explained in the javadoc for {spring-framework-api}/core/Conventions.html[`Conventions`]. You can always assign an explicit name by using the overloaded `addAttribute` method or through the name attribute on `@ModelAttribute` (for a return value). 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 a2b5f261bf75..09b30a4a43cf 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 @@ -1,8 +1,15 @@ [[webflux-ann-requestmapping]] -= Request Mapping += Mapping Requests [.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc[See equivalent in the Servlet stack]# +This section discusses request mapping for annotated controllers. + +[[webflux-ann-requestmapping-annotation]] +== `@RequestMapping` + +[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-annotation[See equivalent in the Servlet stack]# + The `@RequestMapping` annotation is used to map requests to controllers methods. It has various attributes to match by URL, HTTP method, request parameters, headers, and media types. You can use it at the class level to express shared mappings or at the method level @@ -21,6 +28,12 @@ because, arguably, most controller methods should be mapped to a specific HTTP m using `@RequestMapping`, which, by default, matches to all HTTP methods. At the same time, a `@RequestMapping` is still needed at the class level to express shared mappings. +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. + The following example uses type and method level mappings: [tabs] @@ -429,8 +442,14 @@ attributes with a narrower, more specific purpose. `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, and `@PatchMapping` are examples of composed annotations. They are provided, because, arguably, most controller methods should be mapped to a specific HTTP method versus using `@RequestMapping`, -which, by default, matches to all HTTP methods. If you need an example of composed -annotations, look at how those are declared. +which, by default, matches to all HTTP methods. If you need an example of how to implement +a composed annotation, look at how those are declared. + +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. Spring WebFlux also supports custom request mapping attributes with custom request matching logic. This is a more advanced option that requires sub-classing @@ -500,3 +519,90 @@ 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. + +For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @HttpExchange("/persons") + interface PersonService { + + @GetExchange("/{id}") + Person getPerson(@PathVariable Long id); + + @PostExchange + void add(@RequestBody Person person); + } + + @RestController + class PersonController implements PersonService { + + public Person getPerson(@PathVariable Long id) { + // ... + } + + @ResponseStatus(HttpStatus.CREATED) + public void add(@RequestBody Person person) { + // ... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @HttpExchange("/persons") + interface PersonService { + + @GetExchange("/{id}") + fun getPerson(@PathVariable id: Long): Person + + @PostExchange + fun add(@RequestBody person: Person) + } + + @RestController + class PersonController : PersonService { + + override fun getPerson(@PathVariable id: Long): Person { + // ... + } + + @ResponseStatus(HttpStatus.CREATED) + override fun add(@RequestBody person: Person) { + // ... + } + } +---- +====== + +`@HttpExchange` and `@RequestMapping` have differences. +`@RequestMapping` can map to any number of requests by path patterns, HTTP methods, +and more, while `@HttpExchange` declares a single endpoint with a concrete HTTP method, +path, and content types. + +For method parameters and returns values, generally, `@HttpExchange` supports a +subset of the method parameters that `@RequestMapping` does. Notably, it excludes any +server-side specific parameter types. For details, see the list for +xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and +xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping]. 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 new file mode 100644 index 000000000000..59090a205692 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc @@ -0,0 +1,124 @@ +[[mvc-ann-validation]] += Validation + +[.small]#xref:web/webmvc/mvc-controller/ann-validation.adoc[See equivalent in the Servlet stack]# + +Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] 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], +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 `WebExchangeBindException`. + +2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared +directly on method parameters, or on the method (for the return value), then method +validation must be applied, and that supersedes validation at the method argument level +because method validation covers both method parameter constraints and nested constraints +via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`. + +Applications must handle both `WebExchangeBindException` and +`HandlerMethodValidationException` as either may be raised depending on the controller +method signature. The two exceptions, however are designed to be very similar, and can be +handled with almost identical code. The main difference is that the former is for a single +object while the latter is for a list of method parameters. + +NOTE: `@Valid` is not a constraint annotation, but rather for nested constraints within +an Object. Therefore, by itself `@Valid` does not lead to method validation. `@NotNull` +on the other hand is a constraint, and adding it to an `@Valid` parameter leads to method +validation. For nullability specifically, you may also use the `required` flag of +`@RequestBody` or `@ModelAttribute`. + +Method validation may be used in combination with `Errors` or `BindingResult` method +parameters. However, the controller method is called only if all validation errors are on +method parameters with an `Errors` immediately after. If there are validation errors on +any other method parameter then `HandlerMethodValidationException` is raised. + +You can configure a `Validator` globally through the +xref:web/webflux/config.adoc#webflux-config-validation[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. + +NOTE: If a controller has a class level `@Validated`, then +xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method[method validation is applied] +through an AOP proxy. In order to take advantage of the Spring MVC built-in support for +method validation added in Spring Framework 6.1, you need to remove the class level +`@Validated` annotation from the controller. + +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 +by method parameter. You can either iterate over those, or provide a visitor with callback +methods by controller method parameter type: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + HandlerMethodValidationException ex = ... ; + + ex.visitResults(new HandlerMethodValidationException.Visitor() { + + @Override + public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) { + // ... + } + + @Override + public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) { + // ... + } + + @Override + public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) { + + // ... + + @Override + public void other(ParameterValidationResult result) { + // ... + } + }); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + // HandlerMethodValidationException + val ex + + 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/webflux/dispatcher-handler.adoc b/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc index b0919fd8fdab..a621320538ae 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc @@ -12,7 +12,7 @@ This model is flexible and supports diverse workflows. It is also designed to be a Spring bean itself and implements `ApplicationContextAware` for access to the context in which it runs. If `DispatcherHandler` is declared with a bean name of `webHandler`, it is, in turn, discovered by -{api-spring-framework}/web/server/adapter/WebHttpHandlerBuilder.html[`WebHttpHandlerBuilder`], +{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html[`WebHttpHandlerBuilder`], which puts together a request-processing chain, as described in xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API]. Spring configuration in a WebFlux application typically contains: @@ -144,9 +144,9 @@ as a `HandlerResult`, along with some additional context, and passed to the firs | 100 | `ViewResolutionResultHandler` -| `CharSequence`, {api-spring-framework}/web/reactive/result/view/View.html[`View`], - {api-spring-framework}/ui/Model.html[Model], `Map`, - {api-spring-framework}/web/reactive/result/view/Rendering.html[Rendering], +| `CharSequence`, {spring-framework-api}/web/reactive/result/view/View.html[`View`], + {spring-framework-api}/ui/Model.html[Model], `Map`, + {spring-framework-api}/web/reactive/result/view/Rendering.html[Rendering], or any other `Object` is treated as a model attribute. See also xref:web/webflux/dispatcher-handler.adoc#webflux-viewresolution[View Resolution]. @@ -202,13 +202,13 @@ the list of configured `ViewResolver` implementations. trailing slash, and resolve it to a `View`. The same also happens when a view name was not provided (for example, model attribute was returned) or an async return value (for example, `Mono` completed empty). -* {api-spring-framework}/web/reactive/result/view/Rendering.html[Rendering]: API for +* {spring-framework-api}/web/reactive/result/view/Rendering.html[Rendering]: API for view resolution scenarios. Explore the options in your IDE with code completion. * `Model`, `Map`: Extra model attributes to be added to the model for the request. * Any other: Any other return value (except for simple types, as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) is treated as a model attribute to be added to the model. The attribute name is derived -from the class name by using {api-spring-framework}/core/Conventions.html[conventions], +from the class name by using {spring-framework-api}/core/Conventions.html[conventions], unless a handler method `@ModelAttribute` annotation is present. The model can contain asynchronous, reactive types (for example, from Reactor or RxJava). Prior diff --git a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc index 39f080c10178..1b5a9e643a7e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc @@ -6,4 +6,4 @@ HTTP/2 is supported with Reactor Netty, Tomcat, Jetty, and Undertow. However, there are considerations related to server configuration. For more details, see the -https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support[HTTP/2 wiki page]. +{spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page]. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc index e9b72f23173e..b03cefb04bbe 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc @@ -38,13 +38,13 @@ code, it becomes important to control the rate of events so that a fast producer overwhelm its destination. Reactive Streams is a -https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification[small spec] -(also https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html[adopted] in Java 9) +{reactive-streams-spec}[small spec] +(also {java-api}/java.base/java/util/concurrent/Flow.html[adopted] in Java 9) that defines the interaction between asynchronous components with back pressure. For example a data repository (acting as -https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html[Publisher]) +{reactive-streams-site}/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html[Publisher]) can produce data that an HTTP server (acting as -https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html[Subscriber]) +{reactive-streams-site}/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html[Subscriber]) can then write to the response. The main purpose of Reactive Streams is to let the subscriber control how quickly or how slowly the publisher produces data. @@ -63,10 +63,10 @@ low-level. Applications need a higher-level and richer, functional API to compose async logic -- similar to the Java 8 `Stream` API but not only for collections. This is the role that reactive libraries play. -https://github.com/reactor/reactor[Reactor] is the reactive library of choice for +{reactor-github-org}/reactor[Reactor] is the reactive library of choice for Spring WebFlux. It provides the -https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html[`Mono`] and -https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html[`Flux`] API types +{reactor-site}/docs/core/release/api/reactor/core/publisher/Mono.html[`Mono`] and +{reactor-site}/docs/core/release/api/reactor/core/publisher/Flux.html[`Flux`] API types to work on data sequences of 0..1 (`Mono`) and 0..N (`Flux`) through a rich set of operators aligned with the ReactiveX https://reactivex.io/documentation/operators.html[vocabulary of operators]. Reactor is a Reactive Streams library and, therefore, all of its operators support non-blocking back pressure. 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 495dc89ab868..13d527592cc8 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc @@ -13,7 +13,7 @@ request handling, on top of which concrete programming models such as annotated controllers and functional endpoints are built. * For the client side, there is a basic `ClientHttpConnector` contract to perform HTTP requests with non-blocking I/O and Reactive Streams back pressure, along with adapters for -https://github.com/reactor/reactor-netty[Reactor Netty], reactive +{reactor-github-org}/reactor-netty[Reactor Netty], reactive https://github.com/jetty-project/jetty-reactive-httpclient[Jetty HttpClient] and https://hc.apache.org/[Apache HttpComponents]. The higher level xref:web/webflux-webclient.adoc[WebClient] used in applications @@ -26,7 +26,7 @@ deserialization of HTTP request and response content. [[webflux-httphandler]] == `HttpHandler` -{api-spring-framework}/http/server/reactive/HttpHandler.html[HttpHandler] +{spring-framework-api}/http/server/reactive/HttpHandler.html[HttpHandler] is a simple contract with a single method to handle a request and a response. It is intentionally minimal, and its main and only purpose is to be a minimal abstraction over different HTTP server APIs. @@ -39,7 +39,7 @@ The following table describes the supported server APIs: | Netty | Netty API -| https://github.com/reactor/reactor-netty[Reactor Netty] +| {reactor-github-org}/reactor-netty[Reactor Netty] | Undertow | Undertow API @@ -59,7 +59,7 @@ The following table describes the supported server APIs: |=== The following table describes server dependencies (also see -https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-the-Spring-Framework[supported versions]): +{spring-framework-wiki}/What%27s-New-in-the-Spring-Framework[supported versions]): |=== |Server name|Group id|Artifact name @@ -213,7 +213,7 @@ Kotlin:: *Servlet Container* To deploy as a WAR to any Servlet container, you can extend and include -{api-spring-framework}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`] +{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`] in the WAR. That class wraps an `HttpHandler` with `ServletHttpHandlerAdapter` and registers that as a `Servlet`. @@ -224,9 +224,9 @@ that as a `Servlet`. The `org.springframework.web.server` package builds on the xref:web/webflux/reactive-spring.adoc#webflux-httphandler[`HttpHandler`] contract to provide a general-purpose web API for processing requests through a chain of multiple -{api-spring-framework}/web/server/WebExceptionHandler.html[`WebExceptionHandler`], multiple -{api-spring-framework}/web/server/WebFilter.html[`WebFilter`], and a single -{api-spring-framework}/web/server/WebHandler.html[`WebHandler`] component. The chain can +{spring-framework-api}/web/server/WebExceptionHandler.html[`WebExceptionHandler`], multiple +{spring-framework-api}/web/server/WebFilter.html[`WebFilter`], and a single +{spring-framework-api}/web/server/WebHandler.html[`WebHandler`] component. The chain can be put together with `WebHttpHandlerBuilder` by simply pointing to a Spring `ApplicationContext` where components are xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[auto-detected], and/or by registering components @@ -368,26 +368,18 @@ collecting to a `MultiValueMap`. === Forwarded Headers [.small]#xref:web/webmvc/filters.adoc#filters-forwarded-headers[See equivalent in the Servlet stack]# -As a request goes through proxies (such as load balancers), the host, port, and -scheme may change. That makes it a challenge, from a client perspective, to create links that point to the correct -host, port, and scheme. +include::partial$web/forwarded-headers.adoc[] -https://tools.ietf.org/html/rfc7239[RFC 7239] defines the `Forwarded` HTTP header -that proxies can use to provide information about the original request. There are other -non-standard headers, too, including `X-Forwarded-Host`, `X-Forwarded-Port`, -`X-Forwarded-Proto`, `X-Forwarded-Ssl`, and `X-Forwarded-Prefix`. + + +[[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 xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[detected] and used. -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. - 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 @@ -395,6 +387,17 @@ 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. + + + [[webflux-filters]] == Filters [.small]#xref:web/webmvc/filters.adoc[See equivalent in the Servlet stack]# @@ -435,7 +438,7 @@ The following table describes the available `WebExceptionHandler` implementation | `ResponseStatusExceptionHandler` | Provides handling for exceptions of type - {api-spring-framework}/web/server/ResponseStatusException.html[`ResponseStatusException`] + {spring-framework-api}/web/server/ResponseStatusException.html[`ResponseStatusException`] by setting the response to the HTTP status code of the exception. | `WebFluxResponseStatusExceptionHandler` @@ -456,15 +459,15 @@ The `spring-web` and `spring-core` modules provide support for serializing and deserializing byte content to and from higher level objects through non-blocking I/O with Reactive Streams back pressure. The following describes this support: -* {api-spring-framework}/core/codec/Encoder.html[`Encoder`] and -{api-spring-framework}/core/codec/Decoder.html[`Decoder`] are low level contracts to +* {spring-framework-api}/core/codec/Encoder.html[`Encoder`] and +{spring-framework-api}/core/codec/Decoder.html[`Decoder`] are low level contracts to encode and decode content independent of HTTP. -* {api-spring-framework}/http/codec/HttpMessageReader.html[`HttpMessageReader`] and -{api-spring-framework}/http/codec/HttpMessageWriter.html[`HttpMessageWriter`] are contracts +* {spring-framework-api}/http/codec/HttpMessageReader.html[`HttpMessageReader`] and +{spring-framework-api}/http/codec/HttpMessageWriter.html[`HttpMessageWriter`] are contracts to encode and decode HTTP message content. * An `Encoder` can be wrapped with `EncoderHttpMessageWriter` to adapt it for use in a web application, while a `Decoder` can be wrapped with `DecoderHttpMessageReader`. -* {api-spring-framework}/core/io/buffer/DataBuffer.html[`DataBuffer`] abstracts different +* {spring-framework-api}/core/io/buffer/DataBuffer.html[`DataBuffer`] abstracts different byte buffer representations (e.g. 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. @@ -482,7 +485,7 @@ xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs]. [[webflux-codecs-jackson]] === Jackson JSON -JSON and binary JSON (https://github.com/FasterXML/smile-format-specification[Smile]) are +JSON and binary JSON ({jackson-github-org}/smile-format-specification[Smile]) are both supported when the Jackson library is present. The `Jackson2Decoder` works as follows: @@ -546,7 +549,7 @@ for the actual parsing to a `Flux` and then simply collects the parts into By default, the `DefaultPartHttpMessageReader` is used, but this can be changed through the `ServerCodecConfigurer`. For more information about the `DefaultPartHttpMessageReader`, refer to the -{api-spring-framework}/http/codec/multipart/DefaultPartHttpMessageReader.html[javadoc of `DefaultPartHttpMessageReader`]. +{spring-framework-api}/http/codec/multipart/DefaultPartHttpMessageReader.html[javadoc of `DefaultPartHttpMessageReader`]. On the server side where multipart form content may need to be accessed from multiple places, `ServerWebExchange` provides a dedicated `getMultipartData()` method that parses @@ -640,11 +643,11 @@ is not useful for correlating log messages that belong to a specific request. Th WebFlux log messages are prefixed with a request-specific ID by default. On the server side, the log ID is stored in the `ServerWebExchange` attribute -({api-spring-framework}/web/server/ServerWebExchange.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]), +({spring-framework-api}/web/server/ServerWebExchange.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]), while a fully formatted prefix based on that ID is available from `ServerWebExchange#getLogPrefix()`. On the `WebClient` side, the log ID is stored in the `ClientRequest` attribute -({api-spring-framework}/web/reactive/function/client/ClientRequest.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]) +({spring-framework-api}/web/reactive/function/client/ClientRequest.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]) ,while a fully formatted prefix is available from `ClientRequest#logPrefix()`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/security.adoc b/framework-docs/modules/ROOT/pages/web/webflux/security.adoc index 4c448e3cba7b..fcb982d254cb 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/security.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/security.adoc @@ -4,7 +4,7 @@ [.small]#xref:web/webmvc/mvc-security.adoc[See equivalent in the Servlet stack]# -The https://spring.io/projects/spring-security[Spring Security] project provides support +The {spring-site-projects}/spring-security[Spring Security] project provides support for protecting web applications from malicious exploits. See the Spring Security reference documentation, including: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc index 6ea33120fd8c..1f73f66f3ecd 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc @@ -6,19 +6,12 @@ This section describes options for client-side access to REST endpoints. -[[webmvc-resttemplate]] -== `RestTemplate` - -`RestTemplate` is a synchronous client to perform HTTP requests. It is the original -Spring REST client and exposes a simple, template-method API over underlying HTTP client -libraries. +[[webmvc-restclient]] +== `RestClient` -NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only requests for minor -changes and bugs to be accepted. Please, consider using the -xref:web/webflux-webclient.adoc[WebClient] which offers a more modern API and -supports sync, async, and streaming scenarios. +`RestClient` is a synchronous HTTP client that exposes a modern, fluent API. -See xref:integration/rest-clients.adoc[REST Endpoints] for details. +See xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] for more details. @@ -26,23 +19,21 @@ See xref:integration/rest-clients.adoc[REST Endpoints] for details. [[webmvc-webclient]] == `WebClient` -`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was -introduced in 5.0 and offers a modern alternative to the `RestTemplate`, with efficient -support for both synchronous and asynchronous, as well as streaming scenarios. +`WebClient` is a reactive client to perform HTTP requests with a fluent API. + +See xref:web/webflux-webclient.adoc[WebClient] for more details. -In contrast to `RestTemplate`, `WebClient` supports the following: -* Non-blocking I/O. -* Reactive Streams back pressure. -* High concurrency with fewer hardware resources. -* Functional-style, fluent API that takes advantage of Java 8 lambdas. -* Synchronous and asynchronous interactions. -* Streaming up to or streaming down from a server. -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 +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. [[webmvc-http-interface]] == HTTP Interface diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc index 3a8596d8250a..a8e7bb148b64 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc @@ -76,7 +76,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r and, consequently, browsers reject them. Each `HandlerMapping` can be -{api-spring-framework}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] +{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications use the MVC Java configuration or the XML namespace to declare such mappings, which results in a single global map being passed to all `HandlerMapping` instances. @@ -89,7 +89,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, e.g. `allowCredentials` and `maxAge`, the local overrides the global value. See -{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] +{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] for more details. [TIP] @@ -108,7 +108,7 @@ To learn more from the source or make advanced customizations, check the code be == `@CrossOrigin` [.small]#xref:web/webflux-cors.adoc#webflux-cors-controller[See equivalent in the Reactive stack]# -The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] +The {spring-framework-api}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] annotation enables cross-origin requests on annotated controller methods, as the following example shows: @@ -385,7 +385,7 @@ as the following example shows: [.small]#xref:web/webflux-cors.adoc#webflux-cors-webfilter[See equivalent in the Reactive stack]# You can apply CORS support through the built-in -{api-spring-framework}/web/filter/CorsFilter.html[`CorsFilter`]. +{spring-framework-api}/web/filter/CorsFilter.html[`CorsFilter`]. NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc index 8d146310be73..276adcade006 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc @@ -760,6 +760,73 @@ Kotlin:: ====== +[[webmvc-fn-serving-resources]] +== Serving Resources + +WebMvc.fn provides built-in support for serving resources. + +NOTE: In addition to the capabilities described below, it is possible to implement even more flexible resource handling thanks to +{spring-framework-api}++/web/servlet/function/RouterFunctions.html#resources(java.util.function.Function)++[`RouterFunctions#resource(java.util.function.Function)`]. + +[[webmvc-fn-resource]] +=== Redirecting to a resource + +It is possible to redirect requests matching a specified predicate to a resource. This can be useful, for example, +for handling redirects in Single Page Applications. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ClassPathResource index = new ClassPathResource("static/index.html"); + List extensions = Arrays.asList("js", "css", "ico", "png", "jpg", "gif"); + RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate(); + RouterFunction redirectToIndex = route() + .resource(spaPredicate, index) + .build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + 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)) + resource(spaPredicate, index) + } +---- +====== + +[[webmvc-fn-resources]] +=== Serving resources from a root location + +It is also possible to route requests that match a given pattern to resources relative to a given root location. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + Resource location = new FileSystemResource("public-resources/"); + RouterFunction resources = RouterFunctions.resources("/resources/**", location); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val location = FileSystemResource("public-resources/") + val resources = router { resources("/resources/**", location) } +---- +====== + + [[webmvc-fn-running]] == Running a Server [.small]#xref:web/webflux-functional.adoc#webflux-fn-running[See equivalent in the Reactive stack]# diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc index 68714b3dfe5c..d12bf374fa40 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 @@ -102,7 +102,7 @@ cookies or other HTTP headers. The feed is automatically written to the response object after the method returns. For an example of creating an Atom view, see Alef Arendsen's Spring Team Blog -https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support[entry]. +{spring-site-blog}/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support[entry]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc index fd8f53404349..3b419811c325 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc @@ -31,12 +31,12 @@ serializers and deserializers for specific types. [.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]# `MappingJackson2XmlView` uses the -https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML extension's] `XmlMapper` +{jackson-github-org}/jackson-dataformat-xml[Jackson XML extension's] `XmlMapper` to render the response content as XML. If the model contains multiple entries, you should explicitly set the object to be serialized by using the `modelKey` bean property. If the model contains a single entry, it is serialized automatically. -You can customized XML mapping as needed by using JAXB or Jackson's provided +You can customize XML mapping as needed by using JAXB or Jackson's provided annotations. When you need further control, you can inject a custom `XmlMapper` through the `ObjectMapper` property, for cases where custom XML you need to provide serializers and deserializers for specific types. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc index 1e37619d3932..51ad8f57bf73 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc @@ -44,7 +44,7 @@ Spring tags have HTML escaping features to enable or disable escaping of charact The `spring.tld` tag library descriptor (TLD) is included in the `spring-webmvc.jar`. For a comprehensive reference on individual tags, browse the -{api-spring-framework}/web/servlet/tags/package-summary.html#package.description[API reference] +{spring-framework-api}/web/servlet/tags/package-summary.html#package.description[API reference] or see the tag library description. @@ -683,7 +683,7 @@ the HTML would be as follows: ---- What if we want to display the entire list of errors for a given page? The next example -shows that the `errors` tag also supports some basic wildcarding functionality. +shows that the `errors` tag also supports some basic wildcard functionality. * `path="{asterisk}"`: Displays all errors. * `path="lastName"`: Displays all errors associated with the `lastName` field. @@ -745,7 +745,7 @@ The HTML would be as follows: The `spring-form.tld` tag library descriptor (TLD) is included in the `spring-webmvc.jar`. For a comprehensive reference on individual tags, browse the -{api-spring-framework}/web/servlet/tags/form/package-summary.html#package.description[API reference] +{spring-framework-api}/web/servlet/tags/form/package-summary.html#package.description[API reference] or see the tag library description. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc index 27fac970a199..adc87d900352 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc @@ -5,7 +5,7 @@ The Spring Framework has a built-in integration for using Spring MVC with any templating library that can run on top of the -https://www.jcp.org/en/jsr/detail?id=223[JSR-223] Java scripting engine. We have tested the following +{JSR}223[JSR-223] Java scripting engine. We have tested the following templating libraries on different script engines: [%header] @@ -17,7 +17,7 @@ templating libraries on different script engines: |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.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython] -|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |https://kotlinlang.org/[Kotlin] +|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== TIP: The basic rule for integrating any other script engine is that it must implement the @@ -174,7 +174,7 @@ The render function is called with the following parameters: * `String template`: The template content * `Map model`: The view model * `RenderingContext renderingContext`: The - {api-spring-framework}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] + {spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] that gives access to the application context, the locale, the template loader, and the URL (since 5.0) @@ -265,8 +265,8 @@ template engine configuration, for example). The following example shows how to ---- Check out the Spring Framework unit tests, -{spring-framework-main-code}/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and -{spring-framework-main-code}/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources], +{spring-framework-code}/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and +{spring-framework-code}/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources], for more configuration examples. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc.adoc b/framework-docs/modules/ROOT/pages/web/webmvc.adoc index d75c3c842500..8eaac910662d 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc.adoc @@ -7,19 +7,16 @@ Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, "Spring Web MVC," comes from the name of its source module -({spring-framework-main-code}/spring-webmvc[`spring-webmvc`]), +({spring-framework-code}/spring-webmvc[`spring-webmvc`]), but it is more commonly known as "Spring MVC". Parallel to Spring Web MVC, Spring Framework 5.0 introduced a reactive-stack web framework whose name, "Spring WebFlux," is also based on its source module -({spring-framework-main-code}/spring-webflux[`spring-webflux`]). -This chapter covers Spring Web MVC. The xref:testing/unit.adoc#mock-objects-web-reactive[next chapter] -covers Spring WebFlux. +({spring-framework-code}/spring-webflux[`spring-webflux`]). +This chapter covers Spring Web MVC. For reactive-stack web applications, see +xref:web-reactive.adoc[Web on Reactive Stack]. For baseline information and compatibility with Servlet container and Jakarta EE version ranges, see the Spring Framework -https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions[Wiki]. - - - +{spring-framework-wiki}/Spring-Framework-Versions[Wiki]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc index 8851bc74c24a..823171a36968 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc @@ -26,18 +26,16 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method -[[filters-forwarded-headers]] +[[forwarded-headers]] == Forwarded Headers [.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]# -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. +include::partial$web/forwarded-headers.adoc[] -https://tools.ietf.org/html/rfc7239[RFC 7239] defines the `Forwarded` HTTP header -that proxies can use to provide information about the original request. There are other -non-standard headers, too, including `X-Forwarded-Host`, `X-Forwarded-Port`, -`X-Forwarded-Proto`, `X-Forwarded-Ssl`, and `X-Forwarded-Prefix`. + + +[[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 @@ -45,12 +43,22 @@ headers to eliminate further impact. The filter relies on wrapping the request, therefore it must be ordered ahead of other filters, such as `RequestContextFilter`, that should work with the modified and not the original 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. + + +[[filters-forwarded-headers-dispatcher]] +=== Dispatcher Types + In order to support xref:web/webmvc/mvc-ann-async.adoc[asynchronous requests] and error dispatches this filter should be mapped with `DispatcherType.ASYNC` and also `DispatcherType.ERROR`. If using Spring Framework's `AbstractAnnotationConfigDispatcherServletInitializer` @@ -70,13 +78,14 @@ it does the same, but it also compares the computed value against the `If-None-M request header and, if the two are equal, returns a 304 (NOT_MODIFIED). This strategy saves network bandwidth but not CPU, as the full response must be computed for each request. -State-changing HTTP methods and other HTTP conditional request headers such as `If-Match` and `If-Unmodified-Since` are outside the scope of this filter. -Other strategies at the controller level can avoid the computation and have a broader support for HTTP conditional requests. +State-changing HTTP methods and other HTTP conditional request headers such as `If-Match` and +`If-Unmodified-Since` are outside the scope of this filter. Other strategies at the controller level +can avoid the computation and have a broader support for HTTP conditional requests. See xref:web/webmvc/mvc-caching.adoc[HTTP Caching]. This filter has a `writeWeakETag` parameter that configures the filter to write weak ETags similar to the following: `W/"02a2d595e6ed9a0b24f027f2b63b134d6"` (as defined in -https://tools.ietf.org/html/rfc7232#section-2.3[RFC 7232 Section 2.3]). +{rfc-site}/rfc7232#section-2.3[RFC 7232 Section 2.3]). In order to support xref:web/webmvc/mvc-ann-async.adoc[asynchronous requests] this filter must be mapped with `DispatcherType.ASYNC` so that the filter can delay and successfully generate an 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 83276621eba3..d8b00ce3f7eb 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 @@ -92,7 +92,7 @@ Kotlin:: ====== 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] `TaskExecutor`. +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `AsyncTaskExecutor`. @@ -128,7 +128,7 @@ Here is a very concise overview of Servlet asynchronous request processing: * The controller returns a `Callable`. * Spring MVC calls `request.startAsync()` and submits the `Callable` to - a `TaskExecutor` for processing in a separate thread. + an `AsyncTaskExecutor` for processing in a separate thread. * Meanwhile, the `DispatcherServlet` and all filters exit the Servlet container thread, but the response remains open. * Eventually the `Callable` produces a result, and Spring MVC dispatches the request back @@ -137,7 +137,7 @@ Here is a very concise overview of Servlet asynchronous request processing: asynchronously produced return value from the `Callable`. For further background and context, you can also read -https://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support[the +{spring-site-blog}/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support[the blog posts] that introduced asynchronous request processing support in Spring MVC 3.2. @@ -165,11 +165,11 @@ processing (instead of `postHandle` and `afterCompletion`). `HandlerInterceptor` implementations can also register a `CallableProcessingInterceptor` or a `DeferredResultProcessingInterceptor`, to integrate more deeply with the lifecycle of an asynchronous request (for example, to handle a timeout event). See -{api-spring-framework}/web/servlet/AsyncHandlerInterceptor.html[`AsyncHandlerInterceptor`] +{spring-framework-api}/web/servlet/AsyncHandlerInterceptor.html[`AsyncHandlerInterceptor`] for more details. `DeferredResult` provides `onTimeout(Runnable)` and `onCompletion(Runnable)` callbacks. -See the {api-spring-framework}/web/context/request/async/DeferredResult.html[javadoc of `DeferredResult`] +See the {spring-framework-api}/web/context/request/async/DeferredResult.html[javadoc of `DeferredResult`] for more details. `Callable` can be substituted for `WebAsyncTask` that exposes additional methods for timeout and completion callbacks. @@ -399,16 +399,15 @@ Applications can also return `Flux` or `Observable>`. TIP: Spring MVC supports Reactor and RxJava through the -{api-spring-framework}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`] from +{spring-framework-api}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`] from `spring-core`, which lets it adapt from multiple reactive libraries. For streaming to the response, reactive back pressure is supported, but writes to the response are still blocking and are run on a separate thread through the -xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `TaskExecutor`, to avoid -blocking the upstream source (such as a `Flux` returned from `WebClient`). -By default, `SimpleAsyncTaskExecutor` is used for the blocking writes, but that is not -suitable under load. If you plan to stream with a reactive type, you should use the -xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[MVC configuration] to configure a task executor. +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] +`AsyncTaskExecutor`, to avoid blocking the upstream source such as a `Flux` returned +from `WebClient`. + @@ -421,7 +420,7 @@ across multiple threads. The Micrometer https://github.com/micrometer-metrics/context-propagation#context-propagation-library[Context Propagation] library simplifies context propagation across threads, and across context mechanisms such as `ThreadLocal` values, -Reactor https://projectreactor.io/docs/core/release/reference/#context[context], +Reactor {reactor-site}/docs/core/release/reference/#context[context], GraphQL Java https://www.graphql-java.com/documentation/concerns/#context-objects[context], and others. @@ -446,9 +445,8 @@ directly. For example: } ---- -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. @@ -494,20 +492,19 @@ In `web.xml` configuration, you can add `true [[mvc-ann-async-configuration-spring-mvc]] === Spring MVC -The MVC configuration exposes the following options related to asynchronous request processing: +The MVC configuration exposes the following options for asynchronous request processing: * Java configuration: Use the `configureAsyncSupport` callback on `WebMvcConfigurer`. * XML namespace: Use the `` element under ``. You can configure the following: -* Default timeout value for async requests, which if not set, depends -on the underlying Servlet container. +* The default timeout value for async requests depends +on the underlying Servlet container, unless it is set explicitly. * `AsyncTaskExecutor` to use for blocking writes when streaming with -xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] and for executing `Callable` instances returned from -controller methods. We highly recommended configuring this property if you -stream with reactive types or have controller methods that return `Callable`, since -by default, it is a `SimpleAsyncTaskExecutor`. +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] and for +executing `Callable` instances returned from controller methods. +The one used by default is not suitable for production under load. * `DeferredResultProcessingInterceptor` implementations and `CallableProcessingInterceptor` implementations. Note that you can also set the default timeout value on a `DeferredResult`, 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 183e49b0aeb4..89d62147be1a 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc @@ -5,14 +5,14 @@ A common requirement for REST services is to include details in the body of error responses. The Spring Framework supports the "Problem Details for HTTP APIs" -specification, https://www.rfc-editor.org/rfc/rfc7807.html[RFC 7807]. +specification, {rfc-site}/rfc9457[RFC 9457]. The following are the main abstractions for this support: -- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container +- `ProblemDetail` -- representation for an RFC 9457 problem detail; a simple container for both standard fields defined in the spec, and for non-standard ones. - `ErrorResponse` -- contract to expose HTTP error response details including HTTP -status, response headers, and a body in the format of RFC 7807; this allows exceptions to +status, response headers, and a body in the format of RFC 9457; this allows exceptions to encapsulate and expose the details of how they map to an HTTP response. All Spring MVC exceptions implement this. - `ErrorResponseException` -- basic `ErrorResponse` implementation that others @@ -28,7 +28,7 @@ and any `ErrorResponseException`, and renders an error response with a body. [.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-render[See equivalent in the Reactive stack]# You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from -any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: +any `@RequestMapping` method to render an RFC 9457 response. This is processed as follows: - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not @@ -37,7 +37,7 @@ already set. "application/problem+json" over "application/json" when rendering a `ProblemDetail`, and also falls back on it if no compatible media type is found. -To enable RFC 7807 responses for Spring WebFlux exceptions and for any +To enable RFC 9457 responses for Spring WebFlux exceptions and for any `ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] in Spring configuration. The handler has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which @@ -50,7 +50,7 @@ use a protected method to map any exception to a `ProblemDetail`. == Non-Standard Fields [.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-non-standard[See equivalent in the Reactive stack]# -You can extend an RFC 7807 response with non-standard fields in one of two ways. +You can extend an RFC 9457 response with non-standard fields in one of two ways. One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this @@ -67,24 +67,27 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an [[mvc-ann-rest-exceptions-i18n]] -== Internationalization +== Customization and i18n [.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-i18n[See equivalent in the Reactive stack]# -It is a common requirement to internationalize error response details, and good practice -to customize the problem details for Spring MVC exceptions. This is supported as follows: +It is a common requirement to customize and internationalize error response details. +It is also good practice to customize the problem details for Spring MVC exceptions +to avoid revealing implementation details. This section describes the support for that. -- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field -through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]. -The actual message code value is parameterized with placeholders, e.g. -`+"HTTP method {0} not supported"+` to be expanded from the arguments. -- Each `ErrorResponse` also exposes a message code to resolve the "title" field. -- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the -"detail" and the "title" fields. +An `ErrorResponse` exposes message codes for "type", "title", and "detail", as well as +message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves +these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource] +and updates the corresponding `ProblemDetail` fields accordingly. -By default, the message code for the "detail" field is "problemDetail." + the fully -qualified exception class name. Some exceptions may expose additional message codes in -which case a suffix is added to the default message code. The table below lists message -arguments and codes for Spring MVC exceptions: +The default strategy for message codes is as follows: + +* "type": `problemDetail.type.[fully qualified exception class name]` +* "title": `problemDetail.title.[fully qualified exception class name]` +* "detail": `problemDetail.[fully qualified exception class name][suffix]` + +An `ErrorResponse` may expose more than one message code, typically adding a suffix +to the default message code. The table below lists message codes, and arguments for +Spring MVC exceptions: [[mvc-ann-rest-exceptions-codes]] [cols="1,1,2", options="header"] @@ -99,6 +102,11 @@ arguments and codes for Spring MVC exceptions: | (default) | `+{0}+` property name, `+{1}+` property value +| `HandlerMethodValidationException` +| (default) +| `+{0}+` list all validation errors. +Message codes and arguments for each error are also resolved via `MessageSource`. + | `HttpMediaTypeNotAcceptableException` | (default) | `+{0}+` list of supported media types @@ -130,8 +138,7 @@ arguments and codes for Spring MVC exceptions: | `MethodArgumentNotValidException` | (default) | `+{0}+` the list of global errors, `+{1}+` the list of field errors. - Message codes and arguments for each error within the `BindingResult` are also resolved - via `MessageSource`. + Message codes and arguments for each error are also resolved via `MessageSource`. | `MissingRequestHeaderException` | (default) @@ -161,6 +168,10 @@ arguments and codes for Spring MVC exceptions: | (default) | +| `NoResourceFoundException` +| (default) +| + | `TypeMismatchException` | (default) | `+{0}+` property name, `+{1}+` property value @@ -171,8 +182,13 @@ arguments and codes for Spring MVC exceptions: |=== -By default, the message code for the "title" field is "problemDetail.title." + the fully -qualified exception class name. +NOTE: Unlike other exceptions, the message arguments for +`MethodArgumentValidException` and `HandlerMethodValidationException` are baed 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 +xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method-i18n[Customizing Validation Errors] +for more details. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc index f011bad15660..2ae6c92f6c96 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc @@ -19,16 +19,16 @@ This section describes the HTTP caching-related options that are available in Sp == `CacheControl` [.small]#xref:web/webflux/caching.adoc#webflux-caching-cachecontrol[See equivalent in the Reactive stack]# -{api-spring-framework}/http/CacheControl.html[`CacheControl`] provides support for +{spring-framework-api}/http/CacheControl.html[`CacheControl`] provides support for configuring settings related to the `Cache-Control` header and is accepted as an argument in a number of places: -* {api-spring-framework}/web/servlet/mvc/WebContentInterceptor.html[`WebContentInterceptor`] -* {api-spring-framework}/web/servlet/support/WebContentGenerator.html[`WebContentGenerator`] +* {spring-framework-api}/web/servlet/mvc/WebContentInterceptor.html[`WebContentInterceptor`] +* {spring-framework-api}/web/servlet/support/WebContentGenerator.html[`WebContentGenerator`] * xref:web/webmvc/mvc-caching.adoc#mvc-caching-etag-lastmodified[Controllers] * xref:web/webmvc/mvc-caching.adoc#mvc-caching-static-resources[Static Resources] -While https://tools.ietf.org/html/rfc7234#section-5.2.2[RFC 7234] describes all possible +While {rfc-site}/rfc7234#section-5.2.2[RFC 7234] describes all possible directives for the `Cache-Control` response header, the `CacheControl` type takes a use case-oriented approach that focuses on the common scenarios: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc index 36c7d32956e5..aa9f1f8a4277 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc @@ -52,14 +52,10 @@ The following example shows how to achieve the same configuration in XML: ---- -NOTE: Interceptors are not ideally suited as a security layer due to the potential -for a mismatch with annotated controller path matching, which can also match trailing -slashes and path extensions transparently, along with other path matching options. Many -of these options have been deprecated but the potential for a mismatch remains. -Generally, we recommend using Spring Security which includes a dedicated -https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-requestmatcher[MvcRequestMatcher] -to align with Spring MVC path matching and also has a security firewall that blocks many -unwanted characters in URL paths. +WARNING: Interceptors are not ideally suited as a security layer due to the potential for +a mismatch with annotated controller path matching. Generally, we recommend using Spring +Security, or alternatively a similar approach integrated with the Servlet filter chain, +and applied as early as possible. NOTE: The XML config declares interceptors as `MappedInterceptor` beans, and those are in turn detected by any `HandlerMapping` bean, including those from other frameworks. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc index 1ead038524e0..fb1abadc4aca 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc @@ -5,14 +5,13 @@ You can set the `HttpMessageConverter` instances to use in Java configuration, replacing the ones used by default, by overriding -{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`]. +{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`]. You can also customize the list of configured message converters at the end by overriding -{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`]. +{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`]. TIP: In a Spring Boot application, the `WebMvcAutoConfiguration` adds any `HttpMessageConverter` beans it detects, in addition to default converters. Hence, in a -Boot application, prefer to use the -https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/message-converters.html[HttpMessageConverters] +Boot application, prefer to use the {spring-boot-docs}/web.html#web.servlet.spring-mvc.message-converters[HttpMessageConverters] mechanism. Or alternatively, use `extendMessageConverters` to modify message converters at the end. @@ -60,24 +59,24 @@ Kotlin:: ====== In the preceding example, -{api-spring-framework}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`] +{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`] is used to create a common configuration for both `MappingJackson2HttpMessageConverter` and `MappingJackson2XmlHttpMessageConverter` with indentation enabled, a customized date format, and the registration of -https://github.com/FasterXML/jackson-module-parameter-names[`jackson-module-parameter-names`], +{jackson-github-org}/jackson-module-parameter-names[`jackson-module-parameter-names`], Which adds support for accessing parameter names (a feature added in Java 8). This builder customizes Jackson's default properties as follows: -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. It also automatically registers the following well-known modules if they are detected on the classpath: -* https://github.com/FasterXML/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types. -* https://github.com/FasterXML/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types. -* https://github.com/FasterXML/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`. -* https://github.com/FasterXML/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes. +* {jackson-github-org}/jackson-datatype-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`] @@ -86,7 +85,7 @@ dependency in addition to https://search.maven.org/#search%7Cga%7C1%7Ca%3A%22jac Other interesting Jackson modules are available: * https://github.com/zalando/jackson-datatype-money[jackson-datatype-money]: Support for `javax.money` types (unofficial module). -* https://github.com/FasterXML/jackson-datatype-hibernate[jackson-datatype-hibernate]: Support for Hibernate-specific types and properties (including lazy-loading aspects). +* {jackson-github-org}/jackson-datatype-hibernate[jackson-datatype-hibernate]: Support for Hibernate-specific types and properties (including lazy-loading aspects). The following example shows how to achieve the same configuration in XML: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc index eff9db98d660..989ad29c0c5a 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc @@ -5,7 +5,7 @@ You can customize options related to path matching and treatment of the URL. For details on the individual options, see the -{api-spring-framework}/web/servlet/config/annotation/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. +{spring-framework-api}/web/servlet/config/annotation/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. The following example shows how to customize path matching in Java configuration: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc index adb887831e97..e56686152863 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc @@ -4,7 +4,7 @@ [.small]#xref:web/webflux/config.adoc#webflux-config-static-resources[See equivalent in the Reactive stack]# This option provides a convenient way to serve static resources from a list of -{api-spring-framework}/core/io/Resource.html[`Resource`]-based locations. +{spring-framework-api}/core/io/Resource.html[`Resource`]-based locations. In the next example, given a request that starts with `/resources`, the relative path is used to find and serve static resources relative to `/public` under the web application @@ -64,8 +64,8 @@ See also xref:web/webmvc/mvc-caching.adoc#mvc-caching-static-resources[HTTP caching support for static resources]. The resource handler also supports a chain of -{api-spring-framework}/web/servlet/resource/ResourceResolver.html[`ResourceResolver`] implementations and -{api-spring-framework}/web/servlet/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, +{spring-framework-api}/web/servlet/resource/ResourceResolver.html[`ResourceResolver`] implementations and +{spring-framework-api}/web/servlet/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, which you can use to create a toolchain for working with optimized resources. You can use the `VersionResourceResolver` for versioned resource URLs based on an MD5 hash 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 833914f4ab3e..b307b8fc8c8c 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 @@ -6,7 +6,7 @@ By default, if xref:core/validation/beanvalidation.adoc#validation-beanvalidation-overview[Bean Validation] is present on the classpath (for example, Hibernate Validator), the `LocalValidatorFactoryBean` is registered as a global xref:core/validation/validator.adoc[Validator] for use with `@Valid` and -`Validated` on controller method arguments. +`@Validated` on controller method arguments. In Java configuration, you can customize the global `Validator` instance, as the following example shows: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc index 112361f7dba1..e893eaa75832 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc @@ -47,7 +47,7 @@ Kotlin:: In the preceding example, the method accepts a `Model` and returns a view name as a `String`, but many other options exist and are explained later in this chapter. -TIP: Guides and tutorials on https://spring.io/guides[spring.io] use the annotation-based +TIP: Guides and tutorials on {spring-site-guides}[spring.io] use the annotation-based programming model described in this section. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc index 46726f71ad24..90f206a7c44d 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 @@ -62,7 +62,7 @@ Kotlin:: The selectors in the preceding example are evaluated at runtime and may negatively impact performance if used extensively. See the -{api-spring-framework}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] +{spring-framework-api}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] javadoc for more details. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-exceptionhandler.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-exceptionhandler.adoc index 73af9fa6ce46..f3fe7f2be8d6 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 @@ -228,11 +228,11 @@ level, xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[HandlerExceptionResolv See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity]. | `ErrorResponse` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `ProblemDetail` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `String` @@ -271,7 +271,7 @@ see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | Any other return value | If a return value is not matched to any of the above and is not a simple type (as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), by default, it is treated as a model attribute to be added to the model. If it is a simple type, it remains unresolved. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-initbinder.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-initbinder.adoc index 7507980ae1de..9562ac0f7bcc 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 @@ -1,25 +1,27 @@ [[mvc-ann-initbinder]] -= `DataBinder` += `@InitBinder` [.small]#xref:web/webflux/controller/ann-initbinder.adoc[See equivalent in the Reactive stack]# -`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods that -initialize instances of `WebDataBinder`, and those, in turn, can: +`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to +initialize `WebDataBinder` instances that in turn can: -* Bind request parameters (that is, form or query data) to a model object. -* Convert String-based request values (such as request parameters, path variables, -headers, cookies, and others) to the target type of controller method arguments. -* Format model object values as `String` values when rendering HTML forms. +* Bind request parameters to a model object. +* Convert request values from string to object property types. +* Format model object properties as strings when rendering HTML forms. -`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or -Spring `Converter` and `Formatter` components. In addition, you can use the -xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and `Formatter` -types in a globally shared `FormattingConversionService`. +In an `@Controller`, `DataBinder` customizations apply locally within the controller, +or even to a specific model attribute referenced by name through the annotation. +In an `@ControllerAdvice` customizations can apply to all or a subset of controllers. -`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods -do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared -with a `WebDataBinder` argument (for registrations) and a `void` return value. -The following listing shows an example: +You can register `PropertyEditor`, `Converter`, and `Formatter` components in the +`DataBinder` for type conversion. Alternatively, you can use the +xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and +`Formatter` components in a globally shared `FormattingConversionService`. + +`@InitBinder` methods can have many of the same arguments that `@RequestMapping` methods +have, with the notable exception of `@ModelAttribute`. Typically, such methods have a +`WebDataBinder` argument (for registrations) and a `void` return value, for example: [tabs] ====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/arguments.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/arguments.adoc index d1509896fea8..4e3b30ea3c09 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 @@ -135,7 +135,7 @@ and others) and is equivalent to `required=false`. | Any other argument | If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc index 3ea43c3e9713..b0522fdb773c 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc @@ -8,7 +8,7 @@ Spring offers support for the Jackson JSON library. [.small]#xref:web/webflux/controller/ann-methods/jackson.adoc#webflux-ann-jsonview[See equivalent in the Reactive stack]# Spring MVC provides built-in support for -https://www.baeldung.com/jackson-json-view-annotation[Jackson's Serialization Views], +{baeldung-blog}/jackson-json-view-annotation[Jackson's Serialization Views], which allow rendering only a subset of all fields in an `Object`. To use it with `@ResponseBody` or `ResponseEntity` controller methods, you can use Jackson's `@JsonView` annotation to activate a serialization view class, as the following example shows: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc index 83171fc4f036..c96b15a35297 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc @@ -3,7 +3,7 @@ [.small]#xref:web/webflux/controller/ann-methods/matrix-variables.adoc[See equivalent in the Reactive stack]# -https://tools.ietf.org/html/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in +{rfc-site}/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in path segments. In Spring MVC, we refer to those as "`matrix variables`" based on an https://www.w3.org/DesignIssues/MatrixURIs.html["`old post`"] by Tim Berners-Lee, but they can be also be referred to as URI path parameters. 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 1136ab949588..1ad2640d2abb 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,11 +3,8 @@ [.small]#xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Reactive stack]# -You can use the `@ModelAttribute` annotation on a method argument to access an attribute from -the model or have it be instantiated if not present. The model attribute is also overlain with -values from HTTP Servlet request parameters whose names match to field names. This is referred -to as data binding, and it saves you from having to deal with parsing and converting individual -query parameters and form fields. The following example shows how to do so: +The `@ModelAttribute` method parameter annotation binds request parameters onto a model +object. For example: [tabs] ====== @@ -20,7 +17,7 @@ Java:: // method logic... } ---- -<1> Bind an instance of `Pet`. +<1> Bind to an instance of `Pet`. Kotlin:: + @@ -31,30 +28,27 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1> // method logic... } ---- -<1> Bind an instance of `Pet`. +<1> Bind to an instance of `Pet`. ====== -The `Pet` instance above is sourced in one of the following ways: +The `Pet` instance may be: -* Retrieved from the model where it may have been added by a +* Accessed from the model where it could have been added by a xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method]. -* Retrieved from the HTTP session if the model attribute was listed in +* Accessed from the HTTP session if the model attribute was listed in the class-level xref:web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`] annotation. -* Obtained through a `Converter` where the model attribute name matches the name of a - request value such as a path variable or a request parameter (see next example). -* Instantiated using its default constructor. +* Obtained through a `Converter` if the model attribute name matches the name of a + request value such as a path variable or a request parameter (example follows). +* Instantiated through a default constructor. * Instantiated through a "`primary constructor`" with arguments that match to Servlet - request parameters. Argument names are determined through JavaBeans - `@ConstructorProperties` or through runtime-retained parameter names in the bytecode. - -One alternative to using a xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method] to -supply it or relying on the framework to create the model attribute, is to have a -`Converter` to provide the instance. This is applied when the model attribute -name matches to the name of a request value such as a path variable or a request -parameter, and there is a `Converter` from `String` to the model attribute type. -In the following example, the model attribute name is `account` which matches the URI -path variable `account`, and there is a registered `Converter` which -could load the `Account` from a data store: + request parameters. Argument names are determined through runtime-retained parameter + names in the bytecode. + +As mentioned above, a `Converter` may be used to obtain the model object if +the model attribute name matches to the name of a request value such as a path variable or a +request parameter, _and_ there is a compatible `Converter`. In the below example, +the model attribute name `account` matches URI path variable `account`, and there is a +registered `Converter` that perhaps retrieves it from a persistence store: [tabs] ====== @@ -67,7 +61,6 @@ Java:: // ... } ---- -<1> Bind an instance of `Account` using an explicit attribute name. Kotlin:: + @@ -78,19 +71,19 @@ Kotlin:: // ... } ---- -<1> Bind an instance of `Account` using an explicit attribute name. ====== -After the model attribute instance is obtained, data binding is applied. The -`WebDataBinder` class matches Servlet request parameter names (query parameters and form -fields) to field names on the target `Object`. Matching fields are populated after type -conversion is applied, where necessary. For more on data binding (and validation), see -xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see -xref:web/webmvc/mvc-controller/ann-initbinder.adoc[`DataBinder`]. +By default, both constructor and property +xref:core/validation/beans-beans.adoc#beans-binding[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_ +patterns should be set to limit which properties can be set. For further details on this +and example configuration, see +xref:web/webmvc/mvc-controller/ann-initbinder.adoc#mvc-ann-initbinder-model-design[model design]. -Data binding can result in errors. By default, a `BindException` is raised. However, to check -for such errors in the controller method, you can add a `BindingResult` argument immediately next -to the `@ModelAttribute`, as the following example shows: +When using constructor binding, you can customize request parameter names through an +`@BindParam` annotation. For example: [tabs] ====== @@ -98,31 +91,27 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { // <1> - if (result.hasErrors()) { - return "petForm"; + class Account { + + private final String firstName; + + public Account(@BindParam("first-name") String firstName) { + this.firstName = firstName; } - // ... } ---- -<1> Adding a `BindingResult` next to the `@ModelAttribute`. - Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> - if (result.hasErrors()) { - return "petForm" - } - // ... - } + class Account(@BindParam("first-name") val firstName: String) ---- -<1> Adding a `BindingResult` next to the `@ModelAttribute`. ====== +NOTE: The `@BindParam` may also be placed on the fields that correspond to constructor +parameters. While `@BindParam` is supported out of the box, you can also use a +different annotation by setting a `DataBinder.NameResolver` on `DataBinder` + 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: @@ -144,7 +133,7 @@ Java:: } @PostMapping("update") - public String update(@Valid AccountForm form, BindingResult result, + public String update(AccountForm form, BindingResult result, @ModelAttribute(binding=false) Account account) { // <1> // ... } @@ -166,18 +155,53 @@ Kotlin:: } @PostMapping("update") - fun update(@Valid form: AccountForm, result: BindingResult, + fun update(form: AccountForm, result: BindingResult, @ModelAttribute(binding = false) account: Account): String { // <1> // ... } ---- -<1> Setting `@ModelAttribute(binding=false)`. +<1> Setting `@ModelAt\tribute(binding=false)`. +====== + +If data binding results in errors, by default a `MethodArgumentNotValidException` is raised, +but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute` +in order to handle such errors in the controller method. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { // <1> + if (result.hasErrors()) { + return "petForm"; + } + // ... + } +---- +<1> Adding a `BindingResult` next to the `@ModelAttribute`. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> + if (result.hasErrors()) { + return "petForm" + } + // ... + } +---- +<1> Adding a `BindingResult` next to the `@ModelAttribute`. ====== You can automatically apply validation after data binding by adding the -`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation -(xref:core/validation/beanvalidation.adoc[Bean Validation] and -xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example shows how to do so: +`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation. +See xref:core/validation/beanvalidation.adoc[Bean Validation] and +xref:web/webmvc/mvc-config/validation.adoc[Spring validation]. For example: [tabs] ====== @@ -210,10 +234,18 @@ Kotlin:: <1> Validate the `Pet` instance. ====== -Note that using `@ModelAttribute` is optional (for example, to set its attributes). -By default, any argument that is not a simple value type (as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) -and is not resolved by any other argument resolver is treated as if it were annotated -with `@ModelAttribute`. - - +If there is no `BindingResult` parameter after the `@ModelAttribute`, then +`MethodArgumentNotValueException` 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] +_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`. + +WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute` +support described above does not allow proper ahead-of-time inference of related data +binding reflection hints. As a consequence, it is recommended to explicitly annotate +method parameters with `@ModelAttribute` for use in a GraalVM native image. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc index de91ecad48de..5e4addcb3a26 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 @@ -188,8 +188,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- @PostMapping("/") - public String handle(@Valid @RequestPart("meta-data") MetaData metadata, - BindingResult result) { + public String handle(@Valid @RequestPart("meta-data") MetaData metadata, Errors errors) { // ... } ---- @@ -199,12 +198,14 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- @PostMapping("/") - fun handle(@Valid @RequestPart("meta-data") metadata: MetaData, - result: BindingResult): String { + fun handle(@Valid @RequestPart("meta-data") metadata: MetaData, errors: Errors): String { // ... } ---- ====== +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/requestbody.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestbody.adoc index 2c4f316feb9a..e7b22db93b6d 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 @@ -34,6 +34,10 @@ Kotlin:: You can use the xref:web/webmvc/mvc-config/message-converters.adoc[Message Converters] option of the xref:web/webmvc/mvc-config.adoc[MVC Config] to configure or customize message conversion. +NOTE: Form data should be read using xref:web/webmvc/mvc-controller/ann-methods/requestparam.adoc[`@RequestParam`], +not with `@RequestBody` which can't always be used reliably since in the Servlet API, request parameter +access causes the request body to be parsed, and it can't be read again. + You can use `@RequestBody` in combination with `jakarta.validation.Valid` or Spring's `@Validated` annotation, both of which cause Standard Bean Validation to be applied. By default, validation errors cause a `MethodArgumentNotValidException`, which is turned @@ -48,7 +52,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- @PostMapping("/accounts") - public void handle(@Valid @RequestBody Account account, BindingResult result) { + public void handle(@Valid @RequestBody Account account, Errors errors) { // ... } ---- @@ -58,10 +62,13 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- @PostMapping("/accounts") - fun handle(@Valid @RequestBody account: Account, result: BindingResult) { + fun handle(@Valid @RequestBody account: Account, errors: Errors) { // ... } ---- ====== +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/requestparam.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestparam.adoc index ae5e3a0ed4c7..fba75f9309df 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 @@ -61,7 +61,7 @@ Kotlin:: By default, method parameters that use this annotation are required, but you can specify that a method parameter is optional by setting the `@RequestParam` annotation's `required` flag to -`false` or by declaring the argument with an `java.util.Optional` wrapper. +`false` or by declaring the argument with a `java.util.Optional` wrapper. Type conversion is automatically applied if the target method parameter type is not `String`. See xref:web/webmvc/mvc-controller/ann-methods/typeconversion.adoc[Type Conversion]. @@ -72,10 +72,52 @@ values for the same parameter name. When an `@RequestParam` annotation is declared as a `Map` or `MultiValueMap`, without a parameter name specified in the annotation, then the map is populated with the request parameter values for each given parameter name. +The following example shows how to do so with form data processing: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @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",role="secondary"] +---- + @Controller + @RequestMapping("/pets") + class EditPetForm { + + // ... + + @PostMapping("/process", consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE]) + fun processForm(@RequestParam params: MultiValueMap): String { + // ... + } + + // ... + + } +---- +====== Note that use of `@RequestParam` is optional (for example, to set its attributes). By default, any argument that is a simple value type (as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) and is not resolved by any other argument resolver, is treated as if it were annotated with `@RequestParam`. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc index 1cd1816e5d3c..4fb18b0002cf 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 @@ -37,6 +37,13 @@ Kotlin:: all controller methods. This is the effect of `@RestController`, which is nothing more than a meta-annotation marked with `@Controller` and `@ResponseBody`. +A `Resource` object can be returned for file content, copying the `InputStream` +content of the provided resource to the response `OutputStream`. Note that the +`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably +close it after it has been copied to the response. If you are using `InputStreamResource` +for such a purpose, make sure to construct it with an on-demand `InputStreamSource` +(e.g. through a lambda expression that retrieves the actual `InputStream`). + You can use `@ResponseBody` with reactive types. See xref:web/webmvc/mvc-ann-async.adoc[Asynchronous Requests] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] for more details. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc index f311386cabb4..218ac995399e 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 @@ -32,6 +32,18 @@ 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`. + +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`). Also, custom +subclasses of `InputStreamResource` are only supported in combination with a custom +`contentLength()` implementation which avoids consuming the stream for that purpose. + Spring MVC supports using a single value xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive type] to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types for the body. This allows the following types of async responses: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc index 266d6ee1aaae..00d9f862428e 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc @@ -23,11 +23,11 @@ supported for all return values. | For returning a response with headers and no body. | `ErrorResponse` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `ProblemDetail` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `String` @@ -98,7 +98,7 @@ supported for all return values. | Other return values | If a return value remains unresolved in any other way, it is treated as a model attribute, unless it is a simple type as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], in which case it remains unresolved. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-modelattrib-methods.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-modelattrib-methods.adoc index 0f58a97dd8d6..c034514f2760 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 @@ -76,7 +76,7 @@ Kotlin:: NOTE: When a name is not explicitly specified, a default name is chosen based on the `Object` -type, as explained in the javadoc for {api-spring-framework}/core/Conventions.html[`Conventions`]. +type, as explained in the javadoc for {spring-framework-api}/core/Conventions.html[`Conventions`]. You can always assign an explicit name by using the overloaded `addAttribute` method or through the `name` attribute on `@ModelAttribute` (for a return value). 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 b2a787eb4222..fe929fda35e7 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 @@ -1,8 +1,17 @@ [[mvc-ann-requestmapping]] -= Request Mapping += Mapping Requests [.small]#xref:web/webflux/controller/ann-requestmapping.adoc[See equivalent in the Reactive stack]# +This section discusses request mapping for annotated controllers. + + + +[[mvc-ann-requestmapping-annotation]] +== `@RequestMapping` + +[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-annotation[See equivalent in the Reactive stack]# + You can use the `@RequestMapping` annotation to map requests to controllers methods. It has various attributes to match by URL, HTTP method, request parameters, headers, and media types. You can use it at the class level to express shared mappings or at the method level @@ -21,6 +30,12 @@ arguably, most controller methods should be mapped to a specific HTTP method ver using `@RequestMapping`, which, by default, matches to all HTTP methods. A `@RequestMapping` is still needed at the class level to express shared mappings. +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. + The following example has type and method level mappings: [tabs] @@ -95,8 +110,8 @@ at the end of a path. `PathPattern` also restricts the use of `+**+` for matchin path segments such that it's only allowed at the end of a pattern. This eliminates many cases of ambiguity when choosing the best matching pattern for a given request. For full pattern syntax please refer to -{api-spring-framework}/web/util/pattern/PathPattern.html[PathPattern] and -{api-spring-framework}/util/AntPathMatcher.html[AntPathMatcher]. +{spring-framework-api}/web/util/pattern/PathPattern.html[PathPattern] and +{spring-framework-api}/util/AntPathMatcher.html[AntPathMatcher]. Some example patterns: @@ -217,8 +232,8 @@ some external configuration. When multiple patterns match a URL, the best match must be selected. This is done with one of the following depending on whether use of parsed `PathPattern` is enabled for use or not: -* {api-spring-framework}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`] -* {api-spring-framework}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`] +* {spring-framework-api}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`] +* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`] Both help to sort patterns with more specific ones on top. A pattern is more specific if it has a lower count of URI variables (counted as 1), single wildcards (counted as 1), @@ -287,7 +302,7 @@ Many common path extensions are allowed as safe by default. Applications with cu negotiation to avoid having a `Content-Disposition` header added for those extensions. See xref:web/webmvc/mvc-config/content-negotiation.adoc[Content Types]. -See https://pivotal.io/security/cve-2015-5211[CVE-2015-5211] for additional +See {spring-site-cve}/cve-2015-5211[CVE-2015-5211] for additional recommendations related to RFD. @@ -453,11 +468,6 @@ transparently for request mapping. Controller methods do not need to change. A response wrapper, applied in `jakarta.servlet.http.HttpServlet`, ensures a `Content-Length` header is set to the number of bytes written (without actually writing to the response). -`@GetMapping` (and `@RequestMapping(method=HttpMethod.GET)`) are implicitly mapped to -and support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except -that, instead of writing the body, the number of bytes are counted and the `Content-Length` -header is set. - By default, HTTP OPTIONS is handled by setting the `Allow` response header to the list of HTTP methods listed in all `@RequestMapping` methods that have matching URL patterns. @@ -482,8 +492,14 @@ attributes with a narrower, more specific purpose. `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, and `@PatchMapping` are examples of composed annotations. They are provided because, arguably, most controller methods should be mapped to a specific HTTP method versus using `@RequestMapping`, -which, by default, matches to all HTTP methods. If you need an example of composed -annotations, look at how those are declared. +which, by default, matches to all HTTP methods. If you need an example of how to implement +a composed annotation, look at how those are declared. + +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. Spring MVC also supports custom request-mapping attributes with custom request-matching logic. This is a more advanced option that requires subclassing @@ -549,3 +565,90 @@ Kotlin:: +[[mvc-ann-httpexchange-annotation]] +== `@HttpExchange` +[.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 +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: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @HttpExchange("/persons") + interface PersonService { + + @GetExchange("/{id}") + Person getPerson(@PathVariable Long id); + + @PostExchange + void add(@RequestBody Person person); + } + + @RestController + class PersonController implements PersonService { + + public Person getPerson(@PathVariable Long id) { + // ... + } + + @ResponseStatus(HttpStatus.CREATED) + public void add(@RequestBody Person person) { + // ... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @HttpExchange("/persons") + interface PersonService { + + @GetExchange("/{id}") + fun getPerson(@PathVariable id: Long): Person + + @PostExchange + fun add(@RequestBody person: Person) + } + + @RestController + class PersonController : PersonService { + + override fun getPerson(@PathVariable id: Long): Person { + // ... + } + + @ResponseStatus(HttpStatus.CREATED) + override fun add(@RequestBody person: Person) { + // ... + } + } +---- +====== + +`@HttpExchange` and `@RequestMapping` have differences. +`@RequestMapping` can map to any number of requests by path patterns, HTTP methods, +and more, while `@HttpExchange` declares a single endpoint with a concrete HTTP method, +path, and content types. + +For method parameters and returns values, generally, `@HttpExchange` supports a +subset of the method parameters that `@RequestMapping` does. Notably, it excludes any +server-side specific parameter types. For details, see the list for +xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and +xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[@RequestMapping]. 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 new file mode 100644 index 000000000000..0cbe9c3d06a5 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc @@ -0,0 +1,124 @@ +[[mvc-ann-validation]] += Validation + +[.small]#xref:web/webflux/controller/ann-validation.adoc[See equivalent in the Reactive stack]# + +Spring MVC has built-in xref:core/validation/validator.adoc[validation] 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], +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 +method signature. The two exceptions, however are designed to be very similar, and can be +handled with almost identical code. The main difference is that the former is for a single +object while the latter is for a list of method parameters. + +NOTE: `@Valid` is not a constraint annotation, but rather for nested constraints within +an Object. Therefore, by itself `@Valid` does not lead to method validation. `@NotNull` +on the other hand is a constraint, and adding it to an `@Valid` parameter leads to method +validation. For nullability specifically, you may also use the `required` flag of +`@RequestBody` or `@ModelAttribute`. + +Method validation may be used in combination with `Errors` or `BindingResult` method +parameters. However, the controller method is called only if all validation errors are on +method parameters with an `Errors` immediately after. If there are validation errors on +any other method parameter then `HandlerMethodValidationException` is raised. + +You can configure a `Validator` globally through the +xref:web/webmvc/mvc-config/validation.adoc[WebMvc config], or locally through an +xref:web/webmvc/mvc-controller/ann-initbinder.adoc[@InitBinder] method in an +`@Controller` or `@ControllerAdvice`. You can also use multiple validators. + +NOTE: If a controller has a class level `@Validated`, then +xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method[method validation is applied] +through an AOP proxy. In order to take advantage of the Spring MVC built-in support for +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` +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 +by method parameter. You can either iterate over those, or provide a visitor with callback +methods by controller method parameter type: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + HandlerMethodValidationException ex = ... ; + + ex.visitResults(new HandlerMethodValidationException.Visitor() { + + @Override + public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) { + // ... + } + + @Override + public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) { + // ... + } + + @Override + public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) { + + // ... + + @Override + public void other(ParameterValidationResult result) { + // ... + } + }); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + // HandlerMethodValidationException + val ex + + ex.visitResults(object : HandlerMethodValidationException.Visitor { + + override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) { + // ... + } + + override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) { + // ... + } + + override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) { + // ... + } + + // ... + + override fun other(result: ParameterValidationResult) { + // ... + } + }) +---- +====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc index 17a10e537f7f..da03fba9b4a1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc @@ -8,7 +8,7 @@ Servlet 4 containers are required to support HTTP/2, and Spring Framework 5 is c with Servlet API 4. From a programming model perspective, there is nothing specific that applications need to do. However, there are considerations related to server configuration. For more details, see the -https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support[HTTP/2 wiki page]. +{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 diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc index 5b6aca1f70ad..446ae42c0414 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc @@ -4,7 +4,7 @@ [.small]#xref:web/webflux/security.adoc[See equivalent in the Reactive stack]# -The https://spring.io/projects/spring-security[Spring Security] project provides support +The {spring-site-projects}/spring-security[Spring Security] project provides support for protecting web applications from malicious exploits. See the Spring Security reference documentation, including: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc index 173ee07e6797..189ae02988e1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc @@ -70,7 +70,7 @@ NOTE: In addition to using the ServletContext API directly, you can also extend NOTE: For programmatic use cases, a `GenericWebApplicationContext` can be used as an alternative to `AnnotationConfigWebApplicationContext`. See the -{api-spring-framework}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] +{spring-framework-api}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] javadoc for details. The following example of `web.xml` configuration registers and initializes the `DispatcherServlet`: @@ -111,7 +111,7 @@ the lifecycle of the Servlet container, Spring Boot uses Spring configuration to bootstrap itself and the embedded Servlet container. `Filter` and `Servlet` declarations are detected in Spring configuration and registered with the Servlet container. For more details, see the -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-embedded-container[Spring Boot documentation]. +{spring-boot-docs}/web.html#web.servlet.embedded-container[Spring Boot documentation]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc index 64fcc6e1be63..2a8950c073fe 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc @@ -8,7 +8,7 @@ Applications can declare the infrastructure beans listed in xref:web/webmvc/mvc- that are required to process requests. The `DispatcherServlet` checks the `WebApplicationContext` for each special bean. If there are no matching bean types, it falls back on the default types listed in -{spring-framework-main-code}/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties[`DispatcherServlet.properties`]. +{spring-framework-code}/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties[`DispatcherServlet.properties`]. In most cases, the xref:web/webmvc/mvc-config.adoc[MVC Config] is the best starting point. It declares the required beans in either Java or XML and provides a higher-level configuration callback API to diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc index 23e8587b3bde..01cab7f3b7ed 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc @@ -19,7 +19,7 @@ The following table lists the available `HandlerExceptionResolver` implementatio | A mapping between exception class names and error view names. Useful for rendering error pages in a browser application. -| {api-spring-framework}/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html[`DefaultHandlerExceptionResolver`] +| {spring-framework-api}/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html[`DefaultHandlerExceptionResolver`] | Resolves exceptions raised by Spring MVC and maps them to HTTP status codes. See also alternative `ResponseEntityExceptionHandler` and xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc index 95aa38fbd334..f153256002e5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc @@ -1,34 +1,29 @@ [[mvc-handlermapping-interceptor]] = Interception -All `HandlerMapping` implementations support handler interceptors that are useful when -you want to apply specific functionality to certain requests -- for example, checking for -a principal. Interceptors must implement `HandlerInterceptor` from the -`org.springframework.web.servlet` package with three methods that should provide enough -flexibility to do all kinds of pre-processing and post-processing: - -* `preHandle(..)`: Before the actual handler is run -* `postHandle(..)`: After the handler is run -* `afterCompletion(..)`: After the complete request has finished - -The `preHandle(..)` method returns a boolean value. You can use this method to break or -continue the processing of the execution chain. When this method returns `true`, the -handler execution chain continues. When it returns false, the `DispatcherServlet` -assumes the interceptor itself has taken care of requests (and, for example, rendered an -appropriate view) and does not continue executing the other interceptors and the actual -handler in the execution chain. +All `HandlerMapping` implementations support handler interception which is useful when +you want to apply functionality across requests. A `HandlerInterceptor` can implement the +following: + +* `preHandle(..)` -- callback before the actual handler is run that returns a boolean. +If the method returns `true`, execution continues; if it returns `false`, the rest of the +execution chain is bypassed and the handler is not called. +* `postHandle(..)` -- callback after the handler is run. +* `afterCompletion(..)` -- callback after the complete request has finished. + +NOTE: For `@ResponseBody` and `ResponseEntity` controller methods, the response is written +and committed within the `HandlerAdapter`, before `postHandle` is called. That means it is +too late to change the response, such as to add an extra header. You can implement +`ResponseBodyAdvice` and declare it as an +xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it +directly on `RequestMappingHandlerAdapter`. See xref:web/webmvc/mvc-config/interceptors.adoc[Interceptors] in the section on MVC configuration for examples of how to configure interceptors. You can also register them directly by using setters on individual `HandlerMapping` implementations. -`postHandle` method is less useful with `@ResponseBody` and `ResponseEntity` methods for -which the response is written and committed within the `HandlerAdapter` and before -`postHandle`. That means it is too late to make any changes to the response, such as adding -an extra header. For such scenarios, you can implement `ResponseBodyAdvice` and either -declare it as an xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it directly on -`RequestMappingHandlerAdapter`. - - - +WARNING: Interceptors are not ideally suited as a security layer due to the potential for +a mismatch with annotated controller path matching. Generally, we recommend using Spring +Security, or alternatively a similar approach integrated with the Servlet filter chain, +and applied as early as possible. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc index 44844ba9a06b..d23200eb2a91 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 @@ -75,7 +75,7 @@ This resolver variant uses your Servlet container's multipart parser as-is, potentially exposing the application to container implementation differences. By default, it will try to parse any `multipart/` content type with any HTTP method but this may not be supported across all Servlet containers. See the -{api-spring-framework}/web/multipart/support/StandardServletMultipartResolver.html[`StandardServletMultipartResolver`] +{spring-framework-api}/web/multipart/support/StandardServletMultipartResolver.html[`StandardServletMultipartResolver`] javadoc for details and configuration options. ==== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/sequence.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/sequence.adoc index a640ef3fd869..427a4d0ec139 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 @@ -62,8 +62,7 @@ initialization parameters (`init-param` elements) to the Servlet declaration in The exception can then be caught with a `HandlerExceptionResolver` (for example, by using an `@ExceptionHandler` controller method) and handled as any others. - By default, this is set to `false`, in which case the `DispatcherServlet` sets the - response status to 404 (NOT_FOUND) without raising an exception. + As of 6.1, this property is set to `true` and deprecated. Note that, if xref:web/webmvc/mvc-config/default-servlet-handler.adoc[default servlet handling] is also configured, unresolved requests are always forwarded to the default servlet diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/viewresolver.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/viewresolver.adoc index b6728feb674f..27daeeb49137 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/viewresolver.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/viewresolver.adoc @@ -32,7 +32,7 @@ The following table provides more details on the `ViewResolver` hierarchy: | Convenient subclass of `UrlBasedViewResolver` that supports `InternalResourceView` (in effect, Servlets and JSPs) and subclasses such as `JstlView`. You can specify the view class for all views generated by this resolver by using `setViewClass(..)`. - See the {api-spring-framework}/web/reactive/result/view/UrlBasedViewResolver.html[`UrlBasedViewResolver`] + See the {spring-framework-api}/web/reactive/result/view/UrlBasedViewResolver.html[`UrlBasedViewResolver`] javadoc for details. | `FreeMarkerViewResolver` @@ -103,7 +103,7 @@ Servlet/JSP engine. Note that you may also chain multiple view resolvers, instea == Content Negotiation [.small]#xref:web/webflux/dispatcher-handler.adoc#webflux-multiple-representations[See equivalent in the Reactive stack]# -{api-spring-framework}/web/servlet/view/ContentNegotiatingViewResolver.html[`ContentNegotiatingViewResolver`] +{spring-framework-api}/web/servlet/view/ContentNegotiatingViewResolver.html[`ContentNegotiatingViewResolver`] does not resolve views itself but rather delegates to other view resolvers and selects the view that resembles the representation requested by the client. The representation can be determined from the `Accept` header or from a diff --git a/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc b/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc index f565c2250a10..d929c7f985fd 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc @@ -23,20 +23,20 @@ change application code. SockJS consists of: -* The https://github.com/sockjs/sockjs-protocol[SockJS protocol] +* The {sockjs-protocol}[SockJS protocol] defined in the form of executable -https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated tests]. -* The https://github.com/sockjs/sockjs-client/[SockJS JavaScript client] -- a client library for use in browsers. +{sockjs-protocol-site}/sockjs-protocol-0.3.3.html[narrated tests]. +* The {sockjs-client}[SockJS JavaScript client] -- a client library for use in browsers. * SockJS server implementations, including one in the Spring Framework `spring-websocket` module. * A SockJS Java client in the `spring-websocket` module (since version 4.1). SockJS is designed for use in browsers. It uses a variety of techniques to support a wide range of browser versions. For the full list of SockJS transport types and browsers, see the -https://github.com/sockjs/sockjs-client/[SockJS client] page. Transports +{sockjs-client}[SockJS client] page. Transports fall in three general categories: WebSocket, HTTP Streaming, and HTTP Long Polling. For an overview of these categories, see -https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. +{spring-site-blog}/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. The SockJS client begins by sending `GET /info` to obtain basic information from the server. After that, it must decide what transport @@ -130,13 +130,13 @@ The preceding example is for use in Spring MVC applications and should be includ configuration of a xref:web/webmvc/mvc-servlet.adoc[`DispatcherServlet`]. However, Spring's WebSocket and SockJS support does not depend on Spring MVC. It is relatively simple to integrate into other HTTP serving environments with the help of -{api-spring-framework}/web/socket/sockjs/support/SockJsHttpRequestHandler.html[`SockJsHttpRequestHandler`]. +{spring-framework-api}/web/socket/sockjs/support/SockJsHttpRequestHandler.html[`SockJsHttpRequestHandler`]. On the browser side, applications can use the -https://github.com/sockjs/sockjs-client/[`sockjs-client`] (version 1.0.x). It +{sockjs-client}[`sockjs-client`] (version 1.0.x). It emulates the W3C WebSocket API and communicates with the server to select the best transport option, depending on the browser in which it runs. See the -https://github.com/sockjs/sockjs-client/[sockjs-client] page and the list of +{sockjs-client}[sockjs-client] page and the list of transport types supported by browser. The client also provides several configuration options -- for example, to specify which transports to include. @@ -183,7 +183,7 @@ but can be configured to do so. In the future, it may set it by default. See {docs-spring-security}/features/exploits/headers.html#headers-default[Default Security Headers] of the Spring Security documentation for details on how to configure the setting of the `X-Frame-Options` header. You can also see -https://github.com/spring-projects/spring-security/issues/2718[gh-2718] +{spring-github-org}/spring-security/issues/2718[gh-2718] for additional background. ==== @@ -219,7 +219,7 @@ The XML namespace provides a similar option through the `` ele NOTE: During initial development, do enable the SockJS client `devel` mode that prevents the browser from caching SockJS requests (like the iframe) that would otherwise be cached. For details on how to enable it see the -https://github.com/sockjs/sockjs-client/[SockJS client] page. +{sockjs-client}[SockJS client] page. @@ -231,7 +231,7 @@ from concluding that a connection is hung. The Spring SockJS configuration has a called `heartbeatTime` that you can use to customize the frequency. By default, a heartbeat is sent after 25 seconds, assuming no other messages were sent on that connection. This 25-second value is in line with the following -https://tools.ietf.org/html/rfc6202[IETF recommendation] for public Internet applications. +{rfc-site}/rfc6202[IETF recommendation] for public Internet applications. NOTE: When using STOMP over WebSocket and SockJS, if the STOMP client and server negotiate heartbeats to be exchanged, the SockJS heartbeats are disabled. @@ -248,7 +248,7 @@ should consider customizing the settings according to your specific needs. HTTP streaming and HTTP long polling SockJS transports require a connection to remain open longer than usual. For an overview of these techniques, see -https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. +{spring-site-blog}/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. In Servlet containers, this is done through Servlet 3 asynchronous support that allows exiting the Servlet container thread, processing a request, and continuing diff --git a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc index 653f944dfc08..30d18ba93cbe 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc @@ -84,13 +84,13 @@ The preceding example is for use in Spring MVC applications and should be includ in the configuration of a xref:web/webmvc/mvc-servlet.adoc[`DispatcherServlet`]. However, Spring's WebSocket support does not depend on Spring MVC. It is relatively simple to integrate a `WebSocketHandler` into other HTTP-serving environments with the help of -{api-spring-framework}/web/socket/server/support/WebSocketHttpRequestHandler.html[`WebSocketHttpRequestHandler`]. +{spring-framework-api}/web/socket/server/support/WebSocketHttpRequestHandler.html[`WebSocketHttpRequestHandler`]. When using the `WebSocketHandler` API directly vs indirectly, e.g. through the xref:web/websocket/stomp.adoc[STOMP] messaging, the application must synchronize the sending of messages since the underlying standard WebSocket session (JSR-356) does not allow concurrent sending. One option is to wrap the `WebSocketSession` with -{api-spring-framework}/web/socket/handler/ConcurrentWebSocketSessionDecorator.html[`ConcurrentWebSocketSessionDecorator`]. +{spring-framework-api}/web/socket/handler/ConcurrentWebSocketSessionDecorator.html[`ConcurrentWebSocketSessionDecorator`]. @@ -229,34 +229,27 @@ Java initialization API. The following example shows how to do so: [[websocket-server-runtime-configuration]] -== Server Configuration +== Configuring the Server [.small]#xref:web/webflux-websocket.adoc#webflux-websocket-server-config[See equivalent in the Reactive stack]# -Each underlying WebSocket engine exposes configuration properties that control -runtime characteristics, such as the size of message buffer sizes, idle timeout, -and others. +You can configure of the underlying WebSocket server such as input message buffer size, +idle timeout, and more. -For Tomcat, WildFly, and GlassFish, you can add a `ServletServerContainerFactoryBean` to your -WebSocket Java config, as the following example shows: +For Jakarta WebSocket servers, you can add a `ServletServerContainerFactoryBean` to your +Java configuration. For example: [source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { - - @Bean - public ServletServerContainerFactoryBean createWebSocketContainer() { - ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); - container.setMaxTextMessageBufferSize(8192); - container.setMaxBinaryMessageBufferSize(8192); - return container; - } - - } + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(8192); + container.setMaxBinaryMessageBufferSize(8192); + return container; + } ---- -The following example shows the XML configuration equivalent of the preceding example: +Or to your XML configuration: [source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- @@ -277,12 +270,11 @@ The following example shows the XML configuration equivalent of the preceding ex ---- -NOTE: For client-side WebSocket configuration, you should use `WebSocketContainerFactoryBean` -(XML) or `ContainerProvider.getWebSocketContainer()` (Java configuration). +NOTE: For client Jakarta WebSocket configuration, use +ContainerProvider.getWebSocketContainer() in Java configuration, or +`WebSocketContainerFactoryBean` in XML. -For Jetty, you need to supply a pre-configured Jetty `WebSocketServerFactory` and plug -that into Spring's `DefaultHandshakeHandler` through your WebSocket Java config. -The following example shows how to do so: +For Jetty, you can supply a `Consumer` callback to configure the WebSocket server: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -292,62 +284,25 @@ The following example shows how to do so: @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(echoWebSocketHandler(), - "/echo").setHandshakeHandler(handshakeHandler()); + registry.addHandler(echoWebSocketHandler(), "/echo").setHandshakeHandler(handshakeHandler()); } @Bean public DefaultHandshakeHandler handshakeHandler() { - - WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); - policy.setInputBufferSize(8192); - policy.setIdleTimeout(600000); - - return new DefaultHandshakeHandler( - new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); + JettyRequestUpgradeStrategy strategy = new JettyRequestUpgradeStrategy(); + strategy.addWebSocketConfigurer(configurable -> { + policy.setInputBufferSize(8192); + policy.setIdleTimeout(600000); + }); + return new DefaultHandshakeHandler(strategy); } } ---- -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - - - - - - - - - - - - - - - - - - ----- +TIP: When using STOMP over WebSocket, you will also need to configure +xref:web/websocket/stomp/server-config.adoc[STOMP WebSocket transport] +properties. @@ -359,7 +314,7 @@ As of Spring Framework 4.1.5, the default behavior for WebSocket and SockJS is t only same-origin requests. It is also possible to allow all or a specified list of origins. This check is mostly designed for browser clients. Nothing prevents other types of clients from modifying the `Origin` header value (see -https://tools.ietf.org/html/rfc6454[RFC 6454: The Web Origin Concept] for more details). +{rfc-site}/rfc6454[RFC 6454: The Web Origin Concept] for more details). The three possible behaviors are: diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc index 3ab57f8c878e..03745ca29b5d 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc @@ -1,7 +1,7 @@ [[websocket-stomp-authentication-token-based]] = Token Authentication -https://github.com/spring-projects/spring-security-oauth[Spring Security OAuth] +{spring-github-org}/spring-security-oauth[Spring Security OAuth] provides support for token based security, including JSON Web Token (JWT). You can use this as the authentication mechanism in Web applications, including STOMP over WebSocket interactions, as described in the previous @@ -11,13 +11,13 @@ At the same time, cookie-based sessions are not always the best fit (for example in applications that do not maintain a server-side session or in mobile applications where it is common to use headers for authentication). -The https://tools.ietf.org/html/rfc6455#section-10.5[WebSocket protocol, RFC 6455] +The {rfc-site}/rfc6455#section-10.5[WebSocket protocol, RFC 6455] "doesn't prescribe any particular way that servers can authenticate clients during the WebSocket handshake." In practice, however, browser clients can use only standard authentication headers (that is, basic HTTP authentication) or cookies and cannot (for example) provide custom headers. Likewise, the SockJS JavaScript client does not provide a way to send HTTP headers with SockJS transport requests. See -https://github.com/sockjs/sockjs-client/issues/196[sockjs-client issue 196]. +{sockjs-client}/issues/196[sockjs-client issue 196]. Instead, it does allow sending query parameters that you can use to send a token, but that has its own drawbacks (for example, the token may be inadvertently logged with the URL in server logs). @@ -38,7 +38,7 @@ The next example uses server-side configuration to register a custom authenticat interceptor. Note that an interceptor needs only to authenticate and set the user header on the CONNECT `Message`. Spring notes and saves the authenticated user and associate it with subsequent STOMP messages on the same session. The following -example shows how register a custom authentication interceptor: +example shows how to register a custom authentication interceptor: [source,java,indent=0,subs="verbatim,quotes"] ---- diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc index 5866ec3dc521..95af447e597e 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc @@ -6,7 +6,7 @@ Spring Security provides {docs-spring-security}/servlet/integrations/websocket.html#websocket-authorization[WebSocket sub-protocol authorization] that uses a `ChannelInterceptor` to authorize messages based on the user header in them. Also, Spring Session provides -https://docs.spring.io/spring-session/reference/web-socket.html[WebSocket integration] +{docs-spring-session}/web-socket.html[WebSocket integration] that ensures the user's HTTP session does not expire while the WebSocket session is still active. diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc index aa0017e093dc..045d36398b3f 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 @@ -103,9 +103,9 @@ You can also use the WebSocket transport configuration shown earlier to configur maximum allowed size for incoming STOMP messages. In theory, a WebSocket message can be almost unlimited in size. In practice, WebSocket servers impose limits -- for example, 8K on Tomcat and 64K on Jetty. For this reason, STOMP clients -(such as the JavaScript https://github.com/JSteunou/webstomp-client[webstomp-client] -and others) split larger STOMP messages at 16K boundaries and send them as multiple -WebSocket messages, which requires the server to buffer and re-assemble. +such as https://github.com/stomp-js/stompjs[`stomp-js/stompjs`] and others split larger +STOMP messages at 16K boundaries and send them as multiple WebSocket messages, +which requires the server to buffer and re-assemble. Spring's STOMP-over-WebSocket support does this ,so applications can configure the maximum size for STOMP messages irrespective of WebSocket server-specific message diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc index 8ff62f4c6752..433043984338 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc @@ -91,7 +91,7 @@ and xref:web/websocket/stomp/authentication.adoc[Authentication] for more inform For more example code see: -* https://spring.io/guides/gs/messaging-stomp-websocket/[Using WebSocket to build an +* {spring-site-guides}/gs/messaging-stomp-websocket/[Using WebSocket to build an interactive web application] -- a getting started guide. * https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample application. diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay.adoc index 8db28f6dd504..8a59bd83f937 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 @@ -59,7 +59,7 @@ The following example shows the XML configuration equivalent of the preceding ex ---- The STOMP broker relay in the preceding configuration is a Spring -{api-spring-framework}/messaging/MessageHandler.html[`MessageHandler`] +{spring-framework-api}/messaging/MessageHandler.html[`MessageHandler`] that handles messages by forwarding them to an external message broker. To do so, it establishes TCP connections to the broker, forwards all messages to it, and then forwards all messages received from the broker to clients through their diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-simple-broker.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-simple-broker.adoc index 5be3fdb6ae22..cbedc368c2c2 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/message-flow.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/message-flow.adoc index 275ab0a27bd0..1c4e3adb52c0 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/message-flow.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/message-flow.adoc @@ -7,18 +7,18 @@ connected clients. This section describes the flow of messages on the server sid The `spring-messaging` module contains foundational support for messaging applications that originated in https://spring.io/spring-integration[Spring Integration] and was later extracted and incorporated into the Spring Framework for broader use across many -https://spring.io/projects[Spring projects] and application scenarios. +{spring-site-projects}[Spring projects] and application scenarios. The following list briefly describes a few of the available messaging abstractions: -* {api-spring-framework}/messaging/Message.html[Message]: +* {spring-framework-api}/messaging/Message.html[Message]: Simple representation for a message, including headers and payload. -* {api-spring-framework}/messaging/MessageHandler.html[MessageHandler]: +* {spring-framework-api}/messaging/MessageHandler.html[MessageHandler]: Contract for handling a message. -* {api-spring-framework}/messaging/MessageChannel.html[MessageChannel]: +* {spring-framework-api}/messaging/MessageChannel.html[MessageChannel]: Contract for sending a message that enables loose coupling between producers and consumers. -* {api-spring-framework}/messaging/SubscribableChannel.html[SubscribableChannel]: +* {spring-framework-api}/messaging/SubscribableChannel.html[SubscribableChannel]: `MessageChannel` with `MessageHandler` subscribers. -* {api-spring-framework}/messaging/support/ExecutorSubscribableChannel.html[ExecutorSubscribableChannel]: +* {spring-framework-api}/messaging/support/ExecutorSubscribableChannel.html[ExecutorSubscribableChannel]: `SubscribableChannel` that uses an `Executor` for delivering messages. Both the Java configuration (that is, `@EnableWebSocketMessageBroker`) and the XML namespace configuration 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 17279a243e21..89e60da926c2 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 @@ -6,7 +6,7 @@ written to WebSocket sessions. As the channel is backed by a `ThreadPoolExecutor are processed in different threads, and the resulting sequence received by the client may not match the exact order of publication. -If this is an issue, enable the `setPreservePublishOrder` flag, as the following example shows: +To enable ordered publishing, set the `setPreservePublishOrder` flag as follows: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -47,5 +47,22 @@ When the flag is set, messages within the same client session are published to t `clientOutboundChannel` one at a time, so that the order of publication is guaranteed. Note that this incurs a small performance overhead, so you should enable it only if it is required. +The same also applies to messages from the client, which are sent to the `clientInboundChannel`, +from where they are handled according to their destination prefix. As the channel is backed by +a `ThreadPoolExecutor`, messages are processed in different threads, and the resulting sequence +of handling may not match the exact order in which they were received. +To enable ordered publishing, set the `setPreserveReceiveOrder` flag as follows: +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableWebSocketMessageBroker + public class MyConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.setPreserveReceiveOrder(true); + } + } +---- diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc index 92538177177f..7608bb8635bb 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc @@ -1,9 +1,14 @@ [[websocket-stomp-server-config]] -= WebSocket Server += WebSocket Transport -To configure the underlying WebSocket server, the information in -xref:web/websocket/server.adoc#websocket-server-runtime-configuration[Server Configuration] applies. For Jetty, however you need to set -the `HandshakeHandler` and `WebSocketPolicy` through the `StompEndpointRegistry`: +This section explains how to configure the underlying WebSocket server transport. + +For Jakarta WebSocket servers, add a `ServletServerContainerFactoryBean` to your +configuration. For examples, see +xref:web/websocket/server.adoc#websocket-server-runtime-configuration[Configuring the Server] +under the WebSocket section. + +For Jetty WebSocket servers, customize the `JettyRequestUpgradeStrategy` as follows: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -18,16 +23,30 @@ the `HandshakeHandler` and `WebSocketPolicy` through the `StompEndpointRegistry` @Bean public DefaultHandshakeHandler handshakeHandler() { - - WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); - policy.setInputBufferSize(8192); - policy.setIdleTimeout(600000); - - return new DefaultHandshakeHandler( - new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); + JettyRequestUpgradeStrategy strategy = new JettyRequestUpgradeStrategy(); + strategy.addWebSocketConfigurer(configurable -> { + policy.setInputBufferSize(4 * 8192); + policy.setIdleTimeout(600000); + }); + return new DefaultHandshakeHandler(strategy); } } ---- +In addition to WebSocket server properties, there are also STOMP WebSocket transport properties +to customize as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableWebSocketMessageBroker + public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registry) { + registry.setMessageSizeLimit(4 * 8192); + registry.setTimeToFirstMessage(30000); + } + } +---- diff --git a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc new file mode 100644 index 000000000000..9ba46a5f832a --- /dev/null +++ b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc @@ -0,0 +1,122 @@ +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. + +{rfc-site}/rfc7239[RFC 7239] defines the `Forwarded` HTTP header +that proxies can use to provide information about the original request. + + + +[[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`. + + +[[x-forwarded-host]] +==== X-Forwarded-Host + +While not standard, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host[`X-Forwarded-Host: `] +is a de-facto standard header that is used to communicate the original host 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-Host: example.com` can be sent to inform the server that the original host was `example.com`. + + +[[x-forwarded-port]] +==== X-Forwarded-Port + +While not standard, `X-Forwarded-Port: ` is a de-facto standard header that is used to +communicate the original port 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-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) +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 +`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 + +While not standard, https://microsoft.github.io/reverse-proxy/articles/transforms.html#defaults[`X-Forwarded-Prefix: `] +is a de-facto standard header that is used to communicate the original URL path prefix to a +downstream server. + +Use of `X-Forwarded-Prefix` can vary by deployment scenario, and needs to be flexible to +allow replacing, removing, or prepending the path prefix of the target server. + +_Scenario 1: Override path prefix_ + +[subs="-attributes"] +---- +https://example.com/api/{path} -> http://localhost:8080/app1/{path} +---- + +The prefix is the start of the path before the capture group `+{path}+`. For the proxy, +the prefix is `/api` while for the server the prefix is `/app1`. In this case, the proxy +can send `X-Forwarded-Prefix: /api` to have the original prefix `/api` override the +server prefix `/app1`. + +_Scenario 2: Remove path prefix_ + +At times, an application may want to have the prefix removed. For example, consider the +following proxy to server mapping: + +[subs="-attributes"] +---- +https://app1.example.com/{path} -> http://localhost:8080/app1/{path} +https://app2.example.com/{path} -> http://localhost:8080/app2/{path} +---- + +The proxy has no prefix, while applications `app1` and `app2` have path prefixes +`/app1` and `/app2` respectively. The proxy can send ``X-Forwarded-Prefix: `` to +have the empty prefix override server prefixes `/app1` and `/app2`. + +[NOTE] +==== +A common case for this deployment scenario is where licenses are paid per +production application server, and it is preferable to deploy multiple applications per +server to reduce fees. Another reason is to run more applications on the same server in +order to share the resources required by the server to run. + +In these scenarios, applications need a non-empty context root because there are multiple +applications on the same server. However, this should not be visible in URL paths of +the public API where applications may use different subdomains that provides benefits +such as: + +* Added security, e.g. same origin policy +* Independent scaling of applications (different domain points to different IP address) +==== + +_Scenario 3: Insert path prefix_ + +In other cases, it may be necessary to prepend a prefix. For example, consider the +following proxy to server mapping: + +[subs="-attributes"] +---- +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 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 352e63d3c6f3..90ee5468e338 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,29 +1,16 @@ -In the context of web applications, _data binding_ involves the binding of HTTP request -parameters (that is, form data or query parameters) to properties in a model object and -its nested objects. +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. -Only `public` properties following the -https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions] -are exposed for data binding — for example, `public String getFirstName()` and -`public void setFirstName(String)` methods for a `firstName` property. - -TIP: The model object, and its nested object graph, is also sometimes referred to as a +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). -By default, Spring permits binding to all public properties in the model object graph. -This means you need to carefully consider what public properties the model has, since a -client could target any public property path, even some that are not expected to be -targeted for a given use case. - -For example, given an HTTP form data endpoint, a malicious client could supply values for -properties that exist in the model object graph but are not part of the HTML form -presented in the browser. This could lead to data being set on the model object and any -of its nested objects, that is not expected to be updated. - -The recommended approach is to use a _dedicated model object_ that exposes only -properties that are relevant for the form submission. For example, on a form for changing -a user's email address, the model object should declare a minimum set of properties such -as in the following `ChangeEmailForm`. +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: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -51,13 +38,16 @@ as in the following `ChangeEmailForm`. } ---- -If you cannot or do not want to use a _dedicated model object_ for each data -binding use case, you **must** limit the properties that are allowed for data binding. -Ideally, you can achieve this by registering _allowed field patterns_ via the -`setAllowedFields()` method on `WebDataBinder`. +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. -For example, to register allowed field patterns in your application, you can implement an -`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below: +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. +For example: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -74,22 +64,28 @@ For example, to register allowed field patterns in your application, you can imp } ---- -In addition to registering allowed patterns, it is also possible to register _disallowed -field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses. -Please note, however, that an "allow list" is safer than a "deny list". Consequently, -`setAllowedFields()` should be favored over `setDisallowedFields()`. - -Note that matching against allowed field patterns is case-sensitive; whereas, matching -against disallowed field patterns is case-insensitive. In addition, a field matching a -disallowed pattern will not be accepted even if it also happens to match a pattern in the -allowed list. - -[WARNING] -==== -It is extremely important to properly configure allowed and disallowed field patterns -when exposing your domain model directly for data binding purposes. Otherwise, it is a -big security risk. - -Furthermore, it is strongly recommended that you do **not** use types from your domain -model such as JPA or Hibernate entities as the model object in data binding scenarios. -==== +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. + +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: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class MyController { + + @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 d6e94cec95dc..9b7df3674d8a 100644 --- a/framework-docs/modules/ROOT/partials/web/web-uris.adoc +++ b/framework-docs/modules/ROOT/partials/web/web-uris.adoc @@ -240,9 +240,9 @@ Kotlin:: `UriComponentsBuilder` exposes encoding options at two levels: -* {api-spring-framework}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]: +* {spring-framework-api}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]: Pre-encodes the URI template first and then strictly encodes URI variables when expanded. -* {api-spring-framework}/web/util/UriComponents.html#encode--[UriComponents#encode()]: +* {spring-framework-api}/web/util/UriComponents.html#encode--[UriComponents#encode()]: Encodes URI components _after_ URI variables are expanded. Both options replace non-ASCII and illegal characters with escaped octets. However, the first option @@ -389,7 +389,7 @@ template. encode URI component value _after_ URI variables are expanded. * `NONE`: No encoding is applied. -The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historic +The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historical reasons and for backwards compatibility. The `WebClient` relies on the default value in `DefaultUriBuilderFactory`, which was changed from `EncodingMode.URI_COMPONENT` in 5.0.x to `EncodingMode.TEMPLATE_AND_VALUES` in 5.1. diff --git a/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc b/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc index 339aea089be8..8598a214be91 100644 --- a/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc +++ b/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc @@ -1,7 +1,7 @@ [[introduction-to-websocket]] = Introduction to WebSocket -The WebSocket protocol, https://tools.ietf.org/html/rfc6455[RFC 6455], provides a standardized +The WebSocket protocol, {rfc-site}/rfc6455[RFC 6455], provides a standardized way to establish a full-duplex, two-way communication channel between client and server over a single TCP connection. It is a different TCP protocol from HTTP but is designed to work over HTTP, using ports 80 and 443 and allowing re-use of existing firewall rules. diff --git a/framework-docs/package.json b/framework-docs/package.json new file mode 100644 index 000000000000..c3570e2f8a62 --- /dev/null +++ b/framework-docs/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "antora": "3.2.0-alpha.4", + "@antora/atlas-extension": "1.0.0-alpha.2", + "@antora/collector-extension": "1.0.0-alpha.3", + "@asciidoctor/tabs": "1.0.0-beta.6", + "@springio/antora-extensions": "1.11.1", + "@springio/asciidoctor-extensions": "1.0.0-alpha.10" + } +} diff --git a/framework-docs/src/docs/api/dokka-overview.md b/framework-docs/src/docs/api/dokka-overview.md new file mode 100644 index 000000000000..e68dd557ffc0 --- /dev/null +++ b/framework-docs/src/docs/api/dokka-overview.md @@ -0,0 +1,2 @@ +# All Modules +_See also the Java API documentation (Javadoc)._ \ No newline at end of file diff --git a/framework-docs/src/docs/api/overview.html b/framework-docs/src/docs/api/overview.html index e6086dc458d1..6e98d58b9c3d 100644 --- a/framework-docs/src/docs/api/overview.html +++ b/framework-docs/src/docs/api/overview.html @@ -1,7 +1,10 @@

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

+

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

diff --git a/framework-docs/src/docs/asciidoc/anchor-rewrite.properties b/framework-docs/src/docs/asciidoc/anchor-rewrite.properties deleted file mode 100644 index e1e20afb8446..000000000000 --- a/framework-docs/src/docs/asciidoc/anchor-rewrite.properties +++ /dev/null @@ -1,9 +0,0 @@ -aot=core.aot -aot-basics=core.aot.basics -aot-refresh=core.aot.refresh -aot-bean-factory-initialization-contributions=core.aot.bean-factory-initialization-contributions -aot-bean-registration-contributions=core.aot.bean-registration-contributions -aot-hints=core.aot.hints -aot-hints-import-runtime-hints=core.aot.hints.import-runtime-hints -aot-hints-reflective=core.aot.hints.reflective -aot-hints-register-reflection-for-binding=core.aot.hints.register-reflection-for-binding \ No newline at end of file diff --git a/framework-docs/src/docs/asciidoc/images/DataAccessException.png b/framework-docs/src/docs/asciidoc/images/DataAccessException.png deleted file mode 100644 index 746f17399b99..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/DataAccessException.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/aop-proxy-call.png b/framework-docs/src/docs/asciidoc/images/aop-proxy-call.png deleted file mode 100644 index de6be86ed543..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/aop-proxy-call.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/aop-proxy-plain-pojo-call.png b/framework-docs/src/docs/asciidoc/images/aop-proxy-plain-pojo-call.png deleted file mode 100644 index 8ece077d3445..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/aop-proxy-plain-pojo-call.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/container-magic.png b/framework-docs/src/docs/asciidoc/images/container-magic.png deleted file mode 100644 index 2628e59b00e8..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/container-magic.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/message-flow-broker-relay.png b/framework-docs/src/docs/asciidoc/images/message-flow-broker-relay.png deleted file mode 100644 index 3cf93fa1439c..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/message-flow-broker-relay.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/message-flow-simple-broker.png b/framework-docs/src/docs/asciidoc/images/message-flow-simple-broker.png deleted file mode 100644 index 9afd54f57c23..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/message-flow-simple-broker.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.png b/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.png deleted file mode 100644 index 9c4a950caadb..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.svg b/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.svg deleted file mode 100644 index 07148744b549..000000000000 --- a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.svg +++ /dev/null @@ -1,612 +0,0 @@ - - - -image/svg+xmlPage-1DispatcherServlet -Servlet WebApplicationContext -(containing controllers, view resolvers,and other web-related beans) -Controllers -ViewResolver -HandlerMapping -Root WebApplicationContext -(containing middle-tier services, datasources, etc.) -Services -Repositories -Delegates if no bean found - \ No newline at end of file diff --git a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.graffle b/framework-docs/src/docs/asciidoc/images/oxm-exceptions.graffle deleted file mode 100644 index 4b72bf45285b..000000000000 --- a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.graffle +++ /dev/null @@ -1,1619 +0,0 @@ - - - - - ActiveLayerIndex - 0 - ApplicationVersion - - com.omnigroup.OmniGraffle - 137.11.0.108132 - - AutoAdjust - - BackgroundGraphic - - Bounds - {{0, 0}, {756, 553}} - Class - SolidGraphic - ID - 2 - Style - - shadow - - Draws - NO - - stroke - - Draws - NO - - - - CanvasOrigin - {0, 0} - ColumnAlign - 1 - ColumnSpacing - 36 - CreationDate - 2009-09-11 10:15:26 -0400 - Creator - Thomas Risberg - DisplayScale - 1 0/72 in = 1 0/72 in - GraphDocumentVersion - 6 - GraphicsList - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 42 - Points - - {334.726, 144} - {394.042, 102.288} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 41 - Points - - {489.5, 143.713} - {430.452, 102.287} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - Head - - ID - 4 - - ID - 40 - Points - - {230, 217} - {275.683, 175.337} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - Head - - ID - 4 - - ID - 39 - Points - - {430.381, 216.81} - {329.369, 175.19} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - Tail - - ID - 5 - - - - Bounds - {{56, 217}, {249, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 6 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 MarshallingFailureException} - - - - Bounds - {{325.5, 217}, {283.5, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 5 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 UnmarshallingFailureException} - - - - Bounds - {{184, 145}, {217, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 4 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 MarshallingException} - - - - Bounds - {{430, 145}, {239, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 3 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 ValidationFailureException} - - - - Bounds - {{294, 72}, {244, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 1 - Shape - Rectangle - Style - - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\i\fs36 \cf0 XmlMappingException} - - Wrap - NO - - - GridInfo - - GuidesLocked - NO - GuidesVisible - YES - HPages - 1 - ImageCounter - 1 - KeepToScale - - Layers - - - Lock - NO - Name - Layer 1 - Print - YES - View - YES - - - LayoutInfo - - Animate - NO - circoMinDist - 18 - circoSeparation - 0.0 - layoutEngine - dot - neatoSeparation - 0.0 - twopiSeparation - 0.0 - - LinksVisible - NO - MagnetsVisible - NO - MasterSheets - - ModificationDate - 2009-09-11 10:38:54 -0400 - Modifier - Thomas Risberg - NotesVisible - NO - Orientation - 2 - OriginVisible - NO - PageBreaks - YES - PrintInfo - - NSBottomMargin - - float - 41 - - NSLeftMargin - - float - 18 - - NSOrientation - - int - 1 - - NSPaperSize - - size - {792, 612} - - NSRightMargin - - float - 18 - - NSTopMargin - - float - 18 - - - PrintOnePage - - QuickLookPreview - - JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmls - dGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAGVlk1vG0cMhu/zK3h0Dx4Ph/N5rZsA - DRCgqdW0V0GVGhkryZGdoj+/L2e1q4W1cloLhhZrfr0PORx/pU/0lRw+OSaKUei4pt9p - T84m135oS3f3z0yrZ+L2eV7RrbPx9Nfz0ymAQYAN3f2yPq7WTy/flh0dt0jhU2ppoidf - hIIkWu3o7ucd00+HVoRPPFgEriRJTG/hRwupgwVnUYtTDBksxI1ZhION5Csqb3mCGfLk - c56pQeyD3P267pYv27/X94fucNzu1i/H7Uo1+BooRCYfAonrZTJ9AJPHntD9Q6vO0cM9 - BPdJbvVLsaIIDZAhv/kr5wfoBltvwNYRuDrAnngGThTADb4/LohLC3+L79tSbQwZDDMt - oO49W4eEi425+WPXfVw+PW33f737RxuwPex/oMUjvVsg2ZtNDeJIciEPyoO+STGjDLXj - AHLNbqpDZ2RORwwol6S2hr5Swi7aUpVMr8SflNDN53PdkzLeilWj9d7ly1DLbvsnenrY - v19uu2/H9Ws05jtouKDlioYz0KjkzbRPIxq1a2ianY7I0OJraHz1PZq5Jkc0WWqkbFqT - z2g+Lo/PX5Zd9/+7bMQjKkQkPYbt6bqc3lZFT20HSVenNmXrkcK3k/e63R6nMpZxcJsm - s9jQzW/73VnVlT59b4Sxwpqy8PYEw6yJamb/ZYC5YM2pIt1IrxWxWBdlZuomXZrTYy6O - 5GTMx5HCabNSOKDiZIvDNOxIpNjowZdzsTVxNd0waG1P6zpv51CELXtsH8nZuhJzc85W - cvV4x9anINQhYLUpKY6MJNwCfsHbS+8NAn/A7+Ps/I8enKOtjNg7I3LKx4VtFtQwycfI - x6Uw3k3ynb2brNOSNXN4PI6j9hLbNRUrSQ9gwVC5gpA6qT4H6zP2i0qT4kszxWNI1UgW - 6x2msYMdOOutU2zOIKYFzfleB6CzMXqosMTY9lpYnw3dqjaDyjkb9gU2VlAkk2yDr9lN - 5c8CD3oRYOWIzQzYuFYxGTHhlcu2ZqhtFEwQZZJxgWEVJ48rRG3Ra5GId9hBudUVgutp - hZBtKNI35sIblV3noJts9GAnmLaiHMZ8zM4G36gP+YxeA5FTbSTmvLWXw207NwgiwWaf - wCKgOimYP8DoORSvhDWCYN2Ggk3eODD+OVBbNAFDg3fQrBwxoCXjQNRoGpsk/TzMeb/N - YfRoHHB8W22nfE2zL+1AnPJRYyOpn4gLb1SrKj79C2PwIN4KZW5kc3RyZWFtCmVuZG9i - ago1IDAgb2JqCjkyNgplbmRvYmoKMiAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50 - IDMgMCBSIC9SZXNvdXJjZXMgNiAwIFIgL0NvbnRlbnRzIDQgMCBSIC9NZWRpYUJveCBb - MCAwIDc1NiA1NTNdCj4+CmVuZG9iago2IDAgb2JqCjw8IC9Qcm9jU2V0IFsgL1BERiAv - VGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdIC9Db2xvclNwYWNlIDw8IC9DczIg - MTggMCBSCi9DczEgNyAwIFIgPj4gL0ZvbnQgPDwgL0YxLjAgMTkgMCBSIC9GMi4wIDIw - IDAgUiA+PiAvWE9iamVjdCA8PCAvSW0yIDEwIDAgUgovSW0zIDEyIDAgUiAvSW00IDE0 - IDAgUiAvSW01IDE2IDAgUiAvSW0xIDggMCBSID4+ID4+CmVuZG9iagoxMCAwIG9iago8 - PCAvTGVuZ3RoIDExIDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dp - ZHRoIDUyMiAvSGVpZ2h0IDEwNCAvQ29sb3JTcGFjZQoyMSAwIFIgL1NNYXNrIDIyIDAg - UiAvQml0c1BlckNvbXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVh - bQp4Ae3QMQEAAADCoPVPbQhfiEBhwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMvAMDfE4AAQplbmRzdHJlYW0KZW5kb2JqCjExIDAgb2JqCjcz - NAplbmRvYmoKMTIgMCBvYmoKPDwgL0xlbmd0aCAxMyAwIFIgL1R5cGUgL1hPYmplY3Qg - L1N1YnR5cGUgL0ltYWdlIC9XaWR0aCA0NzggL0hlaWdodCAxMDQgL0NvbG9yU3BhY2UK - MjQgMCBSIC9TTWFzayAyNSAwIFIgL0JpdHNQZXJDb21wb25lbnQgOCAvRmlsdGVyIC9G - bGF0ZURlY29kZSA+PgpzdHJlYW0KeAHt0DEBAAAAwqD1T+1pCYhAYcCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOADA0auAAEKZW5kc3RyZWFtCmVuZG9iagox - MyAwIG9iago2NzQKZW5kb2JqCjE0IDAgb2JqCjw8IC9MZW5ndGggMTUgMCBSIC9UeXBl - IC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggNjEyIC9IZWlnaHQgMTA0IC9D - b2xvclNwYWNlCjI3IDAgUiAvU01hc2sgMjggMCBSIC9CaXRzUGVyQ29tcG9uZW50IDgg - L0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7dCBAAAAAMOg+VNf4AiFUGHA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgIE/MOn+AAEKZW5kc3RyZWFtCmVuZG9iagoxNSAwIG9iago4NTYK - ZW5kb2JqCjE2IDAgb2JqCjw8IC9MZW5ndGggMTcgMCBSIC9UeXBlIC9YT2JqZWN0IC9T - dWJ0eXBlIC9JbWFnZSAvV2lkdGggNTQyIC9IZWlnaHQgMTA0IC9Db2xvclNwYWNlCjMw - IDAgUiAvU01hc2sgMzEgMCBSIC9CaXRzUGVyQ29tcG9uZW50IDggL0ZpbHRlciAvRmxh - dGVEZWNvZGUgPj4Kc3RyZWFtCngB7dAxAQAAAMKg9U9tCy+IQGHAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgy8BwaUrgABCmVuZHN0cmVhbQplbmRvYmoKMTcgMCBvYmoKNzYxCmVuZG9i - ago4IDAgb2JqCjw8IC9MZW5ndGggOSAwIFIgL1R5cGUgL1hPYmplY3QgL1N1YnR5cGUg - L0ltYWdlIC9XaWR0aCA1MzIgL0hlaWdodCAxMDQgL0NvbG9yU3BhY2UKMzMgMCBSIC9T - TWFzayAzNCAwIFIgL0JpdHNQZXJDb21wb25lbnQgOCAvRmlsdGVyIC9GbGF0ZURlY29k - ZSA+PgpzdHJlYW0KeAHt0DEBAAAAwqD1T20KP4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMDAa2CIfgABCmVuZHN0 - cmVhbQplbmRvYmoKOSAwIG9iago3NDcKZW5kb2JqCjIyIDAgb2JqCjw8IC9MZW5ndGgg - MjMgMCBSIC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggNTIyIC9I - ZWlnaHQgMTA0IC9Db2xvclNwYWNlCi9EZXZpY2VHcmF5IC9CaXRzUGVyQ29tcG9uZW50 - IDggL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7Z3tT1PZFsZBCqXvLZS2 - 9GVaTgvtaSmdY4sFCtM2bXhHFISpM0LQqhkYkNHYSAZ1MIwSiSI4EF6iyBDRgEPAECVG - zfxrd53CvXOFcrD3fto96/lATDYmez/rl7XXaTlrZWWh0AF0AB1AB9ABdAAdQAfQAXQA - Hfh/HMhGZaADaREB5z/xj3JQGeHAPxE9AQH+CiD2KICzCwS5qIxzQCCA0LJQHAdDkoM9 - CPKEwvw9iVDEO7AfSqEwD+AGHI5hYZ+D3Nw8gEAkFkskEqlUKkNlgAMQSAinWCzKz2dp - 4GaBBSEH7gTAACCQyuRyhVKpQmWIA0qlQi6XAQ9igGGPhSOuiCQIkA9YDmRyhUpVUKhW - FxVpNFoU8Q5oNEVFanVhgUqlkMtYFiAvwBWRGgU2I7AJgeVACRRotLpivd5gNJpQxDtg - NBr0+mKdVgM0KJMsQFpgUUjxEJEEAQoEiRQ4AAyAAZPZYimhrKgMcIAqsVjMJuABYAAW - pBK2XEiNQjZbIwhFkBBUhRqdHiigrKVldgdNO50uFNEOOJ007bCXlVopoEGv0xSqIC2I - hGzdeDgpQEoAEPIlMoVKrdWbLJStjHaWuz0ehmFOogh3AILo8bjLnXSZjbKY9Fq1SiGD - rJArSHE/QEqAYlGcBMFgpkodLreH8Vae8lfXgAIogh1gI1jtP1XpZTxul6OUMhuSKIih - bEyRFLIhJeSLpXKVWmcwW+2uCsbnrw7UBUPhSCQSRRHtAIQwHArWBar9PqbCZbeaDTq1 - Si4V50NSOHg97KUECYCgNVhstJvxVQWC4Wh9Y1NLa9tpFOEOtLW2NDXWR8PBQJWPcdM2 - C5sV5JJUSYElAe4GJYBgttEer782FGlobmvv6OzqjqGId6C7q7Ojva25IRKq9Xs9tI29 - IJQySAqHrge4HPLyJfICjd5spSt8NcFoU+vZc7Efe3r7LsXjl1FEOxCPX+rr7fkxdu5s - a1M0WOOroK1mvaaATQqHrofsE/AECSlBZ6Lsbm9NqL7lTNf5nr741f6fB4euDaOIduDa - 0ODP/VfjfT3nu8601IdqvG47ZdJBUoAnyYOFAns5QJWg0VtKXYw/WN/aEbtw8Ur/4PCN - m4lbIyjCHbiVuHljeLD/ysULsY7W+qCfcZVa9Bq2UoDr4cuPGZMkKAq1JspR4auNAgi9 - 8f6h64mR0Tt3x+6hCHdg7O6d0ZHE9aH+eC+gEK31VTgok7ZQkZKEPJFUqS4221xMVajp - TKz38sBwYuTO2Pj9iYeTKMIdeDhxf3zszkhieOByb+xMU6iKcdnMxWqlVJR3KCcI8kQy - 9nIoc/sCkbauC/GBXxKjY79PTD5+Mv0URbgD008eT078Pjaa+GUgfqGrLRLwucvY60Em - gpLxwO0gEIrlBVoj5fD4v2s4e/7iT8OJ0XsPJqdmZucWFhZRRDuwsDA3OzM1+eDeaGL4 - p4vnzzZ85/c4KKO2QC4WpiBBIocywepkqsPN53quDAIIE4+mZ+eXni2/WEER7cCL5WdL - 87PTjyYAhcErPeeaw9WM0wqFglySggR4dFAXf1Na7oXLIdbXf33ktwePZuYWn6+svlx7 - hSLagbWXqyvPF+dmHj34beR6f18Mrgdveek3xWp4eDiUE+AhUqFmy4TKuvr2H+KDidvj - k9NzS8t/rr1e33iDItqBjfXXa38uL81NT47fTgzGf2ivr6tkCwU1+/BwsE4AEpRAgt3j - DzZ29FwdHhmbmJpdXF59tfFmc2sbRbQDW5tvNl6tLi/OTk2MjQxf7eloDPo9diBBmZIE - qbJIXwIFY6ips7f/xq/jkzPzzwGEze23OyjCHXi7vQkoPJ+fmRz/9UZ/b2dTCErGEn2R - UpoqJ0hVRQaK/rY63NLVN3Dz9v3HfyytrK1vbu+8e7+LItqB9+92tjfX11aW/nh8//bN - gb6ulnD1tzRlKFIdQYLGSNFMTaS1+9Jg4u7E1Nyz1dd/be282/2AItyB3Xc7W3+9Xn02 - NzVxNzF4qbs1UsPQlFFzNAnwEAkkfB8fujX28Mn88sv1zbcAwsdPKKId+Phh993bzfWX - y/NPHo7dGop/z5LgtB5LQlssfm3k3uTMwou1ja2d9wDCZxTRDnz6+OH9ztbG2ouFmcl7 - I9fi8Bh5FAnwpXS+VKUxJnNCChL+RhHswGduEr74+7XsnFz42gE+YnSdDERPxy4PQ054 - urjy6s32zu6HT58JdgG3Dg58/vRhd2f7zauVxaeQE4Yvx05HAydd8CEjfPGQm4Mk8AcS - JIE/seY+KZLA7Q9/VpEE/sSa+6RIArc//FlFEvgTa+6TIgnc/vBnFUngT6y5T4okcPvD - n1UkgT+x5j4pksDtD39WkQT+xJr7pEgCtz/8WUUS+BNr7pMiCdz+8GcVSeBPrLlPiiRw - +8OfVSSBP7HmPimSwO0Pf1aRBP7EmvukSAK3P/xZRRL4E2vukyIJ3P7wZxVJ4E+suU+K - JHD7w59VJIE/seY+KZLA7Q9/VtMhAd+QzWAu0nlDNusYEoh+Zxw3z/2u9IHOnP/11jx2 - 0iC6bUaKzf8PnTSwuw7hbXSO2H663XWw4xbRfbWO3ny6HbewCx/hvfaO3n56XfiwMyfR - 3Te5Np9mZ07s1kt0R16uzafVrVeAHbyJbtLNufm0OngLhNjVn+jO/VybT6+rP076IHqY - B+fm05v0gdN/CJ/ww7X9dKb/5OBEMMKnfnFtP52JYOy8SJwSSPgwwCO3n9aUQJwcSvh0 - UK7tpzE5FKcJEz0u+JjNpzNNGCeMEz1C/JjNpzdhHAoFMYwY1xrMNtrj9deGIg3Nbe0d - nV3dMRTxDnR3dXa0tzU3REK1fq+HtpkNWhgwLmbHSn/RwDsrKxsGS+exM8YBBYuNdjO+ - qkAwHK1vbGppbTuNItyBttaWpsb6aDgYqPIxbtpmARDY+eJ5h0kAFASQFKSAgs5gttpd - FYzPXx2oC4bCkUgkiiLaAQhhOBSsC1T7fUyFy241G3QAghRSguBgSvh3UhDLFGxWMFOl - Dpfbw3grT/mra0ABFMEOsBGs9p+q9DIet8tRSrFXg0oBd0OqlAA5AZKCMF+SREFvslC2 - MtpZ7vZ4GIY5iSLcAQiix+Mud9JlNspi0idBkOQLISUczglspQAoiCQyuapQo9ObzBbK - Wlpmd9C00+lCEe2A00nTDntZqZWymE16naZQJZdJRADCoXqR/etWSApQNEJWkMqVBWqN - Vm8wAg2WEsqKygAHqBILUGA06LUadYFSLoWMwN4NKVLCPgpwQYghLSgLCgEGXbEeeDCa - UMQ7YAQG9MU6wKAQOJBJxHA1HAVCVnYyK8CzZJIFhUoFNKiLijQaLYp4BzSaoiI1UKBS - KZIcQLGYBOHAhwn7rz4kURDkQloAFiRSmVyuUCpVqAxxQKlUyOUyqQTyAZsQoEY4kZ0a - BLgfICuwdSNbLuSLxICDRCqVylAZ4AAEEsIpFosAA8gHLAdHg8CWjXssAAxAA+CQlAhF - vAP7oRSyFOQKjuUg+QjBsnAiJydHwOKAyjAHAIIcNh1w5oP9aoFNDEka2N8Hwf9EZYAD - e9FM/oQA/yfYX/MP+H1UxjnwNZHH30EH0AF0AB1AB9ABdAAdQAfQAXTgaAf+BYU9EtcK - ZW5kc3RyZWFtCmVuZG9iagoyMyAwIG9iagoyNzE5CmVuZG9iagoyOCAwIG9iago8PCAv - TGVuZ3RoIDI5IDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dpZHRo - IDYxMiAvSGVpZ2h0IDEwNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAvQml0c1BlckNv - bXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae2d7U9T2RaH - BQql7y2U03LaTutpS3taS+fYaoXitKRNESm+oDh1FIJWzVSLHY2NzaAOxlEi8Q1HghhF - xohGHSKGqDGjmX/trl3MnTt290jvyf101+8DMfvI/vDkyVq7B9hrwwYMEkACSAAJIAEk - gASQABJAAkgACSCB/wWBOgwSWD+B2hSEfev/TgMGCYgR+FuVejBnPaqt+QV7ymSNGCSw - XgIyGThDdPuqZmXD1vRqksub16LAIIFqBD47Ipc3gY0g2tcs+2xYY2MT6KVQKlUqlVqt - 1mCQQHUCYAh4olQqmpuJZ1+xjCjWAB0SBAO91BqtVqfXGzBIQJyAXq/TajVgmhI0W7Os - WsMsKwY1jBim0eoMhpZWo7GtjWFMGCRQjQDDtLUZja0tBoNOqyGWQS2DhllFMlLFSBEj - hunBL8ZkbmdZi9VqwyCBagSsVgvLtptNDHimL1sGpYxIRvuAWVYMDmIqNRgGgoFdNrvD - sZFzYpBAdQLcRofDbgPTQDOwTK0ix7IqktWRs5hcAUXM0MqYWfCLc7o7PF6e9/n8GCRA - I+Dz8bzX0+F2cuAZa2ZaDVDKFHJy8qcUMihjoFizSqMzGE2szcG5OnjfpkAwKAjCZgwS - oBMAO4LBwCYf3+HiHDbWZDToNFDJGmW0bgllDI77yrJiFjvn9voDQSG0ZWukqxsSxSCB - SgJEja7I1i0hIRjwe92c3VKWTAkHf1ohq4My1qxUaw1Gs8Xu9Pg7hXCkK7o9Fu9NJBJJ - DBKgEQA3euOx7dGuSFjo9HucdovZaNCqlc1QyCqa5VoZU4FiJovDxQeE8LZorDeZ2tE/ - kB7chUECdAKD6YH+Halkbyy6LSwEeJeDVDKtilrIiGPQKfWgmN3FB0ORnniib+fg7qF9 - wwcyGCRQjcCB4X1Duwd39iXiPZFQkHeRdqnXQCGrbJbQKpuaVdoWhrU7+c5wdyzZn967 - P3NoZHTsaDZ7DIMEaASy2aNjoyOHMvv3pvuTse5wJ++0s0wLKWSVzbKuHt5bQBkz2zhP - INQdTw3sGT44MpY9kTuVHz9dwCABGoHT4/lTuRPZsZGDw3sGUvHuUMDD2cxQyOD9RcWB - jLRKOI0xrMPtFyKxVHooc/jI8Vy+cPZc8XwJgwToBM4Xz50t5HPHjxzODKVTsYjgdztY - hpzIoFl+8aq/7Jiu1WTjvJ3hniQoNprNjZ8pliYuXpq8jEECdAKTly5OlIpnxnPZUZAs - 2RPu9HI2U6uO7liTQq03tttdfmFbvH9PZvTYyUKxdHHyytWp69MYJEAncH3q6pXJi6Vi - 4eSx0cye/vg2we+ytxv1akVTZR2TNSk0pFV2BMLRxODw4ezJn4oTk79OTd+8fecuBgnQ - Cdy5fXN66tfJieJPJ7OHhwcT0XCggzRLjQIO/V/2SplcqW0xWTlvMPJd396DR34sFCcu - X5u+NXNv9v79eQwSoBG4f3/23syt6WuXJ4qFH48c3Nv3XSTo5aymFq1STnNMpYXjmNMn - dPXu3D9yPA+KTd24c2/uwcOFx4sYJEAj8Hjh4YO5e3duTIFk+eMj+3f2dgk+JxzItCqa - Y/Cx0tj+jXtTCFplZix3pvTLtRszs/OPFp88XXqGQQI0AktPnyw+mp+duXHtl9KZ3FgG - mmVok/ubdiN8sKysY/DqQmckx7Et21O7f8jmixeuTN+ZfbDw+9LzFy9fYZAAjcDLF8+X - fl94MHtn+sqFYj77w+7U9i3kQGYkHywrzmPgmB4c8wQjsR1DIycKpcmpW/fmF548e/lq - +fUKBgnQCLxefvXy2ZOF+Xu3piZLhRMjQztikaAHHNPTHVPr29iNcOSP9+8bzZ39+cr0 - zNwjUGx55c0qBgnQCbxZWQbJHs3NTF/5+WxudF9/HA79G9k2vZpax9SGNgvHf9vVOzA8 - dvLchas3f3uwuPRieWX17bv3GCRAI/Du7erK8oulxQe/3bx64dzJseGB3q5vec7SZqjm - GGPleKE7kT5wNF+8NHVr9uGT53+8Xn37/gMGCdAJvH+7+vqP508ezt6aulTMHz2QTnQL - PGdlRByDVxfg2PfZ8fOT12/PLTx9sfwGFPvzIwYJ0Aj8+eH92zfLL54uzN2+Pnl+PPs9 - cczn/Lpjg5ns6dLl6Zn7j5devl59B4p9wiABGoGPf354t/r65dLj+zPTl0uns/Dyoqpj - 8Ks9zWoDYy3XMYpjf2GQQCWBT+KO/fO3resaGuHHlfCa3785mtyVOVaAOnZ3fvHZq5XV - 9x8+fqrcHVeQABD49PHD+9WVV88W5+9CHSscy+xKRjf74UU//MCysQEdQ0mkE0DHpDPE - HcQJoGPifPCpdALomHSGuIM4AXRMnA8+lU4AHZPOEHcQJ4COifPBp9IJoGPSGeIO4gTQ - MXE++FQ6AXRMOkPcQZwAOibOB59KJ4COSWeIO4gTQMfE+eBT6QTQMekMcQdxAuiYOB98 - Kp0AOiadIe4gTgAdE+eDT6UTQMekM8QdxAmgY+J88Kl0AuiYdIa4gzgBdEycDz6VTgAd - k84QdxAngI6J88Gn0gmgY9IZ4g7iBNAxcT74VDoBdEw6Q9xBnAA6Js4Hn0onUJNjeKeK - dOD/fzvUdKfKhq84RrsXCNeQgPi9PV/OgPiPu6HwjjvadW64RiHw39xxh3d10q+kxNUq - BGq+qxPvHKbdq4tr1QnUfOcw3p1Ovx8cV6sTqPHudJwBQZtygGtiBGqdAYGzbGjTWnBN - jEBts2xkOJOLNnQK10QJ1DaTSybH2YK06Xm4JkagxtmCOCOVNgQU10QJ1DgjFWc906cZ - 46oYgZpmPTfgzHr6VHZcFSNQ08z6BjIkFYY9c97OcE8yPZQZzebGzxRLExcvTV7GIAE6 - gclLFydKxTPjuexoZiid7Al3ejkY9UxGpDZUzEgljmkNDOtw+4VILAWSHT5yPJcvnD1X - PF/CIAE6gfPFc2cL+dzxI4dBsVQsIvjdDpYxwKjnSsdgYJJcodEbzTbOEwh1x1MDe4YP - joxlT+RO5cdPFzBIgEbg9Hj+VO5Edmzk4PCegVS8OxTwcDazUa9RyBvr/znKZsOGunpZ - ExSyFoa1O/nOcHcs2Z/euz9zaGR07Gg2ewyDBGgEstmjY6MjhzL796b7k7HucCfvtLNM - C5SxJhnFMWiWSihkJovdxQdDkZ54om/n4O6hfcMHMhgkUI3AgeF9Q7sHd/Yl4j2RUJB3 - 2S0mKGNK0ior61hDIylkBpDM4eIDQnhbNNabTO3oH0gP7sIgATqBwfRA/45UsjcW3RYW - ArzLAYqR01gTxTHSLKGQqUEys8Xu9Pg7hXCkK7o9Fu9NJBJJDBKgEQA3euOx7dGuSFjo - 9HucdosZFFNDGatsleRARgqZUqMjlczOub3+QFAIbdka6eqGRDFIoJIAUaMrsnVLSAgG - /F43RxqlQQedklrGwDEoZPJmVVky1ubgXB28b1MgGBQEYTMGCdAJgB3BYGCTj+9wcQ4b - W1ZM1SyHMlZxHIPf7odCBpIpVBqtoZUxsza7g3O6Ozxenvf5/BgkQCPg8/G819PhdnIO - u401M60GrUYF7y1klSd+8gckUMigW0IlU2v1LUbGxFqs4JljI+fEIIHqBLiNDvDLamFN - jLFFr1VDFSOdklbGPksG7VIJpUzf0gqamdtZMM1qwyCBagSsYBfbbgbBWsEwjUoJjbKq - YhvqypUMDv5ly3QGA3hmbGtjGBMGCVQjwDBtbUbwy2DQlQ2D435ZsS9fjn3+W8uyZLJG - KGVgmUqt0Wp1er0BgwTECej1Oq1Wo1ZBDSNFDM5i9XVVFINuCZWMnPzJsaxZoQTRVGq1 - WoNBAtUJgCHgiVKpAMGghhHDRBQjB/81y0Az8AxEK0eBQQLVCHx2RE78apR93bDyx0ti - WX1DQ4OMiIZBAusjAHo1kBImXsM+n8pIMSt7Rr4BAt+KQQLVCaxpUv4K5vzbonX9A74B - gwTWS2BdTuF/QgJIAAkgASSABJAAEkACSAAJIAEkUDuBfwFWtww3CmVuZHN0cmVhbQpl - bmRvYmoKMjkgMCBvYmoKMzAwNwplbmRvYmoKMzEgMCBvYmoKPDwgL0xlbmd0aCAzMiAw - IFIgL1R5cGUgL1hPYmplY3QgL1N1YnR5cGUgL0ltYWdlIC9XaWR0aCA1NDIgL0hlaWdo - dCAxMDQgL0NvbG9yU3BhY2UKL0RldmljZUdyYXkgL0JpdHNQZXJDb21wb25lbnQgOCAv - RmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAHtnetPU+kWxrkUer9B2S29TMtu - ueyW0tlSLFCclrQBykXkOnUUghTNwICMxkYyqINhlEgUwYFwiSJDRAMOAUOUGDXzr521 - CzlzhLKZnk/nvHs9H4zJWz+sZ/1ca+1e3pWWhkIH0AF0AB1AB9ABdAAdQAfQAXTg/9GB - dJRAHEiJTvAk429looh14O8sZ0DS/wEkB2SAHyJRFkoQDohEkG4OlNMASbBxAEa2WCw5 - kBRFpAOH6RWLs+E/ASByCh+HbGRlZQMYUplMLpcrFAolilAHILmQYplMKpFwhPDzwcGR - Cf0E0AAwFEqVSq3RaFEEO6DRqFUqJTAiA0AO+DihvSTggLrBsaFUqbXanFydLi+PovQo - Ih2gqLw8nS43R6tVq5QcH1A/oL0kx4OrHFzh4NjQABmU3pBvNJrMZguKSAfMZpPRmG/Q - U0CIJsEHlA8OjyQPLwk4YOCQK4ANQAO4sFhttgLajiLUAbrAZrNagBEABPhQyLnxIzke - 6dzMIZZC4dDmUgYjkEHbC4uKSxjG6XShiHPA6WSYkuKiQjsNhBgNVK4WyodUzM2mx4sH - lA6AQyJXqrU6vdFiox1FjLPU7fGwLHsGRaADkFiPx13qZIoctM1i1Ou0aiVUjyxRkt4C - pQMGUlkCDpOVLixxuT1secVZX1U1yI8izAEuq1W+sxXlrMftKimkraYEHjIYTZMUj3Qo - HRKZQqXVGUxWe7GrjPX6qvznAsHaUCgURhHnAKS1Nhg456/yedkyV7HdajLotCqFTALF - 42hrOSgdcoBDb7I5GDfrrfQHasN1DZGm5pbzKAIdaGluijTUhWsD/kov62YcNq56qOTJ - igdHB/QVDcBhdTCecl9NMFTf2NLa3tHVHUUR6UB3V0d7a0tjfShY4yv3MA6uuWiUUDyO - tRZoLNkSuSqHMlrtTJm3OhCONLd1Ri/19Pb1x2IDKOIciMX6+3p7LkU725oj4UC1t4yx - W41UDlc8jrWW9Ax4moXSYbDQxe7y6mBd04Wuiz19sWuDPw2PXB9FEefA9ZHhnwavxfp6 - LnZdaKoLVpe7i2mLAYoHPNUeHTy4xgJTB2W0FbpYX6CuuT16+crVweHRm7fit8dQBDpw - O37r5ujw4NUrl6PtzXUBH+sqtBkpbvKA1vL126UJOtS5egtdUuatCQMcvbHBkRvxsfG7 - 9ybuowh0YOLe3fGx+I2RwVgv4BGu8ZaV0BZ9rjopHdlShUaXb3W42Mpg5EK0d2BoND52 - d2LywdSjaRSBDjyaejA5cXcsPjo00Bu9EAlWsi6HNV+nUUizj9UOUbZUyTWWIrfXH2rp - uhwb+jk+PvHb1PSTp7PPUAQ6MPv0yfTUbxPj8Z+HYpe7WkJ+r7uIay1KKYylRzqLSCxT - 5ejNdInH911928UrP47Gx+8/nJ6Zm19YWlpGEefA0tLC/NzM9MP74/HRH69cbKv/zucp - oc36HJVMnIQOuQrGDruTrapt7Oy5OgxwTD2enV9ceb76cg1FnAMvV5+vLM7PPp4CPIav - 9nQ21laxTjsMHip5EjrgkUWX/01haTk0lmjf4I2xXx8+nltYfrG2/mrjNYo4BzZera+9 - WF6Ye/zw17Ebg31RaC3lpYXf5OvgoeVY7YAHWrWOGzsqztW1/hAbjt+ZnJ5dWFn9Y+PN - 5tZbFHEObG2+2fhjdWVhdnryTnw49kNr3bkKbvDQcQ8tR+cOoEMDdBR7fIGG9p5ro2MT - UzPzy6vrr7febu/soohzYGf77dbr9dXl+ZmpibHRaz3tDQGfpxjo0CSlQ6HJMxbAUBqM - dPQO3vxlcnpu8QXAsb37bg9FoAPvdrcBjxeLc9OTv9wc7O2IBGEsLTDmaRTJaodCm2ei - mW+rapu6+oZu3Xnw5PeVtY3N7d299x/2UcQ58OH93u725sbayu9PHty5NdTX1VRb9S1D - m/K0J9BBmWmGrQ41d/cPx+9NzSw8X3/z587e+/2PKAId2H+/t/Pnm/XnCzNT9+LD/d3N - oWqWoc3UyXTAAy3Q8X1s5PbEo6eLq682t98BHJ8+o4hz4NPH/ffvtjdfrS4+fTRxeyT2 - PUeH034qHS3R2PWx+9NzSy83tnb2PgAcX1DEOfD508cPeztbGy+X5qbvj12PwSPtSXTA - B/gShZYyJ2pHEjr+QhHmwBd+Or767mB6ZhZ8zAJvlbrO+MPnowOjUDueLa+9fru7t//x - 8xfCnMFwwIEvnz/u7+2+fb22/Axqx+hA9HzYf8YFb5bCBy1ZmUiHsCFBOoSdf/7okQ5+ - f4R9inQIO//80SMd/P4I+xTpEHb++aNHOvj9EfYp0iHs/PNHj3Tw+yPsU6RD2Pnnjx7p - 4PdH2KdIh7Dzzx890sHvj7BPkQ5h558/eqSD3x9hnyIdws4/f/RIB78/wj5FOoSdf/7o - kQ5+f4R9inQIO//80SMd/P4I+xTpEHb++aNHOvj9EfYp0iHs/PNHj3Tw+yPsU6RD2Pnn - jx7p4PdH2Kep0IG/shYYK6n8yjrtFDqIu58AA+L/Df6R22z/44YGvN2FuKtckgT0X9zu - gjdDEXgF1AkhpXozFN4qR9zdcScHlOqtcngjJYH3Tp4cUmo3UuJttsTdWMsXUIq32eJN - 2MTdds0XUEo3YYvwFn3iLsrnDSilW/RFYtzAQdyWDb6AUtvAgdt7iFvQwxtQatt7cPMX - gdu9+EJKZfNXJm4NJHAzIF9IqWwN5PbR4sZRAheLnhhSShtHcVsxgRuJ+UJKYVsxbjon - bpX5KQGlsuk8PUOUDe945FBGq50p81YHwpHmts7opZ7evv5YbABFnAOxWH9fb8+laGdb - cyQcqPaWMXarkcqBnYGwjvara9LT0tK5VecypUanN1kdjKfcVxMM1Te2tLZ3dHVHUUQ6 - 0N3V0d7a0lgfCtb4yj2Mw2rS6zRKmSQrMxkdXPHQAh42B+NmvZX+QG24riHS1NxyHkWg - Ay3NTZGGunBtwF/pZd2MwwZwaLnScZwOKB4iKB4KwMNgstqLXWWs11flPxcI1oZCoTCK - OAcgrbXBwDl/lc/LlrmK7VaTAeBQQOk41lgOWks29BY1Vz2sdGGJy+1hyyvO+qqqQX4U - YQ5wWa3yna0oZz1uV0khzbUVrRr6SrLSAbUDiodYIk/gYbTYaEcR4yx1ezwsy55BEegA - JNbjcZc6mSIHbbMYE3DIJWIoHUfHDviSKcylgIdUrlRpcymD0WK10fbCouIShnE6XSji - HHA6GaakuKjQTtusFqOBytWqlHIpwHFsJuW+gQzFIzMrG6qHQqXJ0VF6o8kMhNgKaDuK - UAfoAhuQYTYZ9ZQuR6NSQOXg+kqS0nGIBzQXGZQPTU4uAGLINwIjZguKSAfMwIUx3wBo - 5AIbSrkM2spJcKSlJ6pHVrYkwYdaqwVCdHl5FKVHEekAReXl6YAMrVadYAMG0gQcR94K - O/xpSwIPURaUD+BDrlCqVGqNRosi2AGNRq1SKRVyqBtc4YCZIyM9ORzQW6B6cLMpN35I - pDJARK5QKJQoQh2A5EKKZTIpoAF1g2PjZDi40fSADwAECAFEEpKiiHTgML1ijows0als - JB5dOD4yMjMzRRwiKAE4AGBkcmWDt24cTh9cAUkQwr0eBP8SRagDBxlO/AlJ/zcA/+Qv - 8HqUIBz4JzTga9ABdAAdQAfQAXQAHUAH0AF0AB3433PgX6y7qcQKZW5kc3RyZWFtCmVu - ZG9iagozMiAwIG9iagoyNzYyCmVuZG9iagoyNSAwIG9iago8PCAvTGVuZ3RoIDI2IDAg - UiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dpZHRoIDQ3OCAvSGVpZ2h0 - IDEwNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAvQml0c1BlckNvbXBvbmVudCA4IC9G - aWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae2d/U8T2RfGeSn0fToD7bRM222Z - Uui0lO4IWAFdIBAUAV9Q3LorBK2ahQW7GhubRV0Mq8RGEVwIL1FkiWjAJWCIErOa/de+ - Z4rZXaEdvt2f7iTn+cFoLiaH58Nz723pnJOTg0IH0AF0AB1AB9ABdAAdQAfQgf/mQC5K - IQ5kxRe+p7x/lI8i1oF/KOUBtP8D8g5Z+H5UqgKUIhxQqQCXBHo/wCm2O2AL1WrNjrQo - Ih34jEetLoQfQkC8D9/PbAsKCgGsVqfT6/UGg8GIItQBgAOIdDqtRiMRlucrwc2H/RjQ - AliDkaJMNM2gCHaApk0UZQTGOgC8wzfD9pyCC7mV2BopE8MUFZvNFgvLWlFEOsCyFovZ - XFzEMCbKKPGF/ML2nB6vlFwpuBJbGsiyVlsJx9kdDieKSAccDjvHldisLBCmU3whvhLe - NJfnFFw4cPUGYAtogavT5XaX8h4UoQ7wpW63ywmMATDwNeil4zc93lzpzFVrIbhMMWvj - gCzv8ZZX+ATB7w+giHPA7xcEX0W518MDYc7GFjMQX61aulvtDS9EF+Bq9EYTY7ZyTjdf - Vi74K4OhkCiKB1AEOgBgQqFgpV8oL+PdTs5qZkxGSG+BKs3eDNGFC5UuBdfu4r2+QDAk - VtceDNfVgxpQhDkgUakLH6ytFkPBgM/Lu+wpvDq4WqUJby5EV6MzUIzZZnd5KgJVYk24 - ruFIY1NzS0tLK4o4BwBLc1PjkYa6cI1YFajwuOw2M0MZdBoI7+6teSe6eoBrtbvLhKBY - c6ihsbm17Vh7R2fXCRSBDnR1drQfa2ttbmw4VCMGhTK3lF5Kny68El3Yl2mA6yoTQtXh - w00tR493new+03MugiLSgXM9Z7pPdh0/2tJ0OFwdEsqkzZk2Qnj3bM2wMRdq9FQRy7k8 - QlVNfWNre+fps5Hve/v6L0Wjl1HEORCNXurv6/0+cvZ0Z3trY31NleBxcWyRFN49W3Nu - HrwagujanHxFsLq+qa3jVM/53v7o1YEfh4avxVDEOXBteOjHgavR/t7zPac62prqq4MV - vNMG4YVXRbsPXmljhlOX5dzegBhubOvsjly4eGVgKHbjZvxWAkWgA7fiN2/EhgauXLwQ - 6e5sawyLAa+bY6WTF7bmL9+uStE1FVudvK+q5nArwO2LDgxfjydG7twdvYci0IHRu3dG - EvHrwwPRPsDberimysc7rcWmtHQLtQbaXOIqC4iHmtpPRfouD8biiTujY/fHHyZRBDrw - cPz+2OidRDw2eLkvcqq96ZAYKHOVmGmDtnBPdlWFWqO0MZcHaxpaunouRAd/io+M/jqe - fPxk8imKQAcmnzxOjv86OhL/aTB6oaerpaEmWC5tzUYtXKt27cwqtY4qsjp4Xyj8zdHT - 5y/+EIuP3HuQnJianpmbm0cR58Dc3Mz01ETywb2ReOyHi+dPH/0mHPLxDmsRpVOnoaun - 4Nj1+MW65uNne68MAdzxR5PTswvPFl8soYhz4MXis4XZ6clH44B36Erv2ePNdaLfAwcv - pU9DF67M5pKvvJXVsDFH+geuJ3558GhqZv750vLLlVco4hxYebm89Hx+ZurRg18S1wf6 - I7A1V1d6vyoxw6V5T3bhBZHJLB27tUfaTn4XHYrfHktOziws/r7yenXtDYo4B9ZWX6/8 - vrgwM5kcux0fin53su1IrXTwmqVL8+5zF+jSQLciFG481t17NZYYHZ+Ynl9cfrX2Zn1j - E0WcAxvrb9ZeLS/OT0+MjyZiV3u7jzWGQxVAl05L10BbuFK4VDW1n+kbuPHzWHJq9jnA - Xd98u4Ui0IG3m+uA9/nsVHLs5xsDfWfam+BaVcpZaEO67BoYi50Xvq5r7ujpH7x5+/7j - 3xaWVlbXN7fevd9GEefA+3dbm+urK0sLvz2+f/vmYH9PR3Pd1wJvtzAZ6LIOXhDrWzrP - XRqK3x2fmHm2/PqPja132x9QBDqw/W5r44/Xy89mJsbvxocunetsqRcF3sFmpgsviIDu - t9HhW6MPn8wuvlxdfwtw//yIIs6BPz9sv3u7vvpycfbJw9Fbw9FvJbp+z750uyLRa4l7 - yam5FytrG1vvAe4nFHEOfPzzw/utjbWVF3NTyXuJa1F4SZSJLvwCUGNgWEcqu2no/oUi - zIFP8nS/+OxNbn4BvM0Mb1UFDjS0nohcjkF2n84vvXqzubX94eMnwr4zLAcc+PTxw/bW - 5ptXS/NPIbuxy5ETrQ0HAvBmFbzRXJCPdJX9Q4J0lc1PvnqkK++PsleRrrL5yVePdOX9 - UfYq0lU2P/nqka68P8peRbrK5idfPdKV90fZq0hX2fzkq0e68v4oexXpKpuffPVIV94f - Za8iXWXzk68e6cr7o+xVpKtsfvLVI115f5S9inSVzU++eqQr74+yV5GusvnJV4905f1R - 9irSVTY/+eqRrrw/yl5FusrmJ1890pX3R9mrSFfZ/OSrR7ry/ih7NRu6+JSYwlhn85RY - zj50iXu+EQuSfwZwVzeyfz3hiU9nE/codpqC/sPT2dhZgcAWChlKyrazAnZFIa73SeaC - su2Kgh2NCOxblLmk7DoaYTcy4jqOyRWUZTcy7CRIXLdAuYKy6iSowi6gxDX6lC0oqy6g - KjV28CWuS69cQdl18MXu28Q12JYtKLvu29g5n8Du+HIlZdM5Px+nXhA42UKupGymXkjz - iHBiDYGDaTKWlNXEGpw2ReBEKbmSspg2hZPiiBsFt09B2UyKwymPxI1x3Keg7KY84oRW - IsewyhSVzYRWaTA6TlcmcIpyppKymK6cg5PRiZt9Ll9QNpPRga4UXp3RJM1Gd/FeXyAY - EqtrD4br6kENKMIckKjUhQ/WVouhYMDn5aXB2YwJJmcX7h2dnQN081QFao0+hZdzuvmy - csFfGQyFRFE8gCLQAQATCgUr/UJ5Ge92cim4eo26QJW3e7gyfMgKwgt4tXojxRSzNs7p - cvMeb3mFTxD8/gCKOAf8fkHwVZR7Pbzb5eRsbDFDGfUwN1u1Z+q99Ak6CC/szZBeA0UX - mVkrZ3cAYXcp70ER6gBf6gayDjtnZc1FNGWA5Er7cprofsYLm7MO4ksXFQNgWwkHjB1O - FJEOOIArV2IDtMXA1qjXwbacCW5Obiq9cLVK8TUxDBA2Wywsa0UR6QDLWixmIMswphRb - uFCl4H4x8eLvDzan8KoKIL7AV28wUpSJphkUwQ7QtImijAY95FYKLpy5ebnp4cLeDOmV - 7lbS8avR6gCx3mAwGFGEOgBwAJFOpwW0kFuJbWa40tVqhy8ABsKAOCUtikgHPuNRS2QL - VPuyTV2dJb55+fn5KgkxSgEOANh8Kbayuf3X+ZsiLH09CP4nilAHdgil/szNeN7+zfWL - v8DXoxThwBfY8B/oADqADqAD6AA6gA6gA+gAOpCFA/8DclEtHwplbmRzdHJlYW0KZW5k - b2JqCjI2IDAgb2JqCjI1NTgKZW5kb2JqCjM0IDAgb2JqCjw8IC9MZW5ndGggMzUgMCBS - IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggNTMyIC9IZWlnaHQg - MTA0IC9Db2xvclNwYWNlCi9EZXZpY2VHcmF5IC9CaXRzUGVyQ29tcG9uZW50IDggL0Zp - bHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7Z3rT1PpFsZBCqX3Fnqjl2nZbaG7 - pXS2LZZSmLZpwx1REKbOCEGrZmBARmMjGdTBMEokiuBAuESRIaIBh4AhSoya86+dtQvn - zBHKxp7k5CTvXs8HY7Lxw3rWL8+7drHvyslBoQPoADqADqAD6AA6gA6gA+gAOvD/ciAX - RbQDWXEFTpz4W3kowhz4u7cnoNVfgcYeD+CCQJCPItgBgQCazOJxHBZpIvZwKBAKC/ck - QhHkwH5ThcICAB7AOIaKfSLy8wsAB5FYLJFIpFKpDEWUA9BSaKxYLCosZLngpoJFIg9O - DAACcJDK5HKFUqlCEeeAUqmQy2VAhhiw2KPiiAMkjQRkBEuETK5QqYqK1WqNRqvVoQhy - QKvVaNTq4iKVSiGXsVRAVsABkhkKNiXYkGCJUAIPWp2+xGAwmkxmFEEOmExGg6FEr9MC - F8o0FRAVLBQZXkDSSMAgIZECEQAE0GC2WK2llA1FlANUqdVqMQMZgAVQIZWwY0VmKHLZ - WUIogpBQFWv1BuCBsjnKyp007XK5UYQ44HLRtLO8zGGjgAuDXlusgqgQCdlJ83BQQEwA - EoUSmUKl1hnMVspeRrsqPF4vwzAnUcQ4AO30ej0VLrrMTlnNBp1apZBBUuQLMpweEBMw - XorTSBgtlMPp9ngZX9WpQLAGFEIR4QDby2DgVJWP8XrcTgdlMaahEMOgmSEociEmCsVS - uUqtN1ps5e5Kxh8IhurCkWgsFoujCHEAmhmNhOtCwYCfqXSX2yxGvVoll4oLISgOHh57 - MSEBJHRGq532MP7qUDgar29samltO40ixoG21pamxvp4NByq9jMe2m5lk0IuyRQULBNw - cigBCYud9voCtZFYQ3Nbe0dnV3cCRZAD3V2dHe1tzQ2xSG3A56Xt7PGhlEFQHDo84Ogo - KJTIi7QGi42u9NeE402tZ88lfuzp7buUTF5GEeJAMnmpr7fnx8S5s61N8XCNv5K2WQza - IjYoDh0euSfgPRRiQm+myj2+mkh9y5mu8z19yav9Pw8OXRtGEeLAtaHBn/uvJvt6zned - aamP1Pg85ZRZD0EB76MHBwr26IBpQmuwOtxMIFzf2pG4cPFK/+DwjZupWyMoYhy4lbp5 - Y3iw/8rFC4mO1vpwgHE7rAYtO1HA4fHlR5lpJhTFOjPlrPTXxgGJ3mT/0PXUyOidu2P3 - UMQ4MHb3zuhI6vpQf7IXoIjX+iudlFlXrMjIRIFIqlSXWOxupjrSdCbRe3lgODVyZ2z8 - /sTDSRQxDjycuD8+dmckNTxwuTdxpilSzbjtlhK1UioqOJQTggKRjD06yjz+UKyt60Jy - 4JfU6NjvE5OPn0w/RRHjwPSTx5MTv4+Npn4ZSF7oaouF/J4y9vCQiWDIPHB2CIRieZHO - RDm9ge8azp6/+NNwavTeg8mpmdm5hYVFFCEOLCzMzc5MTT64N5oa/uni+bMN3wW8Tsqk - K5KLhRmYkMhhnLC5mGC0+VzPlUFAYuLR9Oz80rPlFysoQhx4sfxsaX52+tEEQDF4pedc - czTIuGwwUMglGZiA1w51yTeOCh8cHYm+/usjvz14NDO3+Hxl9eXaKxQhDqy9XF15vjg3 - 8+jBbyPX+/sScHj4KhzflKjhxeNQTsCrqELNjhNVdfXtPyQHU7fHJ6fnlpb/XHu9vvEG - RYgDG+uv1/5cXpqbnhy/nRpM/tBeX1fFDhRq9sXj4DwBTCiBiXJvINzY0XN1eGRsYmp2 - cXn11cabza1tFCEObG2+2Xi1urw4OzUxNjJ8taejMRzwlgMTyoxMSJUaQymMmJGmzt7+ - G7+OT87MPwckNrff7qCIceDt9iZA8Xx+ZnL81xv9vZ1NERgySw0apTRTTkhVGiNFfxuM - tnT1Ddy8ff/xH0sra+ub2zvv3u+iCHHg/bud7c31tZWlPx7fv31zoK+rJRr8lqaMGtUR - TGhNFM3UxFq7Lw2m7k5MzT1bff3X1s673Q8oYhzYfbez9dfr1WdzUxN3U4OXultjNQxN - mbRHMwGvosDE98mhW2MPn8wvv1zffAtIfPyEIsSBjx92373dXH+5PP/k4ditoeT3LBMu - 27FMtCWS10buTc4svFjb2Np5D0h8RhHiwKePH97vbG2svViYmbw3ci0JL6NHMQG/Ki+U - qrSmdE5kYOIfKCIc+MzNxBf/+y43Lx9+3QEfY7pPhuKnE5eHISeeLq68erO9s/vh02ci - /MAiwIHPnz7s7my/ebWy+BRyYvhy4nQ8dNINH2TCLzzy85AJPkKCTPCx69w1IxPc/vDx - KTLBx65z14xMcPvDx6fIBB+7zl0zMsHtDx+fIhN87Dp3zcgEtz98fIpM8LHr3DUjE9z+ - 8PEpMsHHrnPXjExw+8PHp8gEH7vOXTMywe0PH58iE3zsOnfNyAS3P3x8ikzwsevcNSMT - 3P7w8Skywceuc9eMTHD7w8enyAQfu85dMzLB7Q8fnyITfOw6d83IBLc/fHyKTPCx69w1 - IxPc/vDxaTZM4HeIeUFINt8hzjmGCUK+aY9lcH+v/MCdqf9x1wDeSULIBSQZyvgv7iTB - u4uIuaToiEKyvbsI7zgj5Cazo8vI9o4zvAuRmBsPjy4ku7sQ8c5UQu5F5SojyztT8W5l - Qu5P5iojq7uVBXgHOyHXrHOWkdUd7AIh7mogZB8DVxnZ7WrAnS6ErG3hLCO7nS64+4mY - /U5chWSz+ykPd8QRsweOq5BsdsSx+0VxlyQxKyOPLCSrXZK4c5aYvbJchWSxcxZ3UxOy - fPqYMrLZTY077AlZUn9MGdntsIeBQgxL7HVGi532+gK1kVhDc1t7R2dXdwJFkAPdXZ0d - 7W3NDbFIbcDnpe0Wow5W2IvZdeVfXMGek5MLC8sL2C32AIXVTnsYf3UoHI3XNza1tLad - RhHjQFtrS1NjfTwaDlX7GQ9ttwIS7Ab7gsNMABQCCAopQKE3Wmzl7krGHwiG6sKRaCwW - i6MIcQCaGY2E60LBgJ+pdJfbLEY9ICGFmBAcjIl/BYVYpmCTwkI5nG6Pl/FVnQoEa0Ah - FBEOsL0MBk5V+Rivx+10UOzBoVLAyZEpJiAnICiEhZI0FAazlbKX0a4Kj9fLMMxJFDEO - QDu9Xk+Fiy6zU1azIY2EpFAIMXE4J9iJAqAQSWRyVbFWbzBbrJTNUVbupGmXy40ixAGX - i6ad5WUOG2W1mA16bbFKLpOIAIlDEyb7/3UhKGDMhKSQypVFaq3OYDQBF9ZSyoYiygGq - 1Ao8mIwGnVZdpJRLISXYkyNDTOxDAceHGKJCWVQMWOhLDECGyYwiyAET0GAo0QMQxUCE - TCKGg+MoJHJy00kBb6RpKhQqFXCh1mi0Wh2KIAe0Wo1GDTyoVIo0ETBeppE48OHE/lc9 - 0lAI8iEqgAqJVCaXK5RKFYo4B5RKhVwuk0ogI9iQgFniRG5mJOD0gKRgJ012rCgUiQEM - iVQqlaGIcgBaCo0Vi0UABGQES8TRSLCD5h4VgAVwAWCkJUIR5MB+U4UsD/mCY4lIv36w - VJzIy8sTsGCgiHUAcMhjI4IzI/anCjYs0lywPw+Cf4kiyoG9vqb/hFb/u+1f8xf4eRTB - DnwNA/gz6AA6gA6gA+gAOoAOoAPoADqADvxvHPgnR1HeRgplbmRzdHJlYW0KZW5kb2Jq - CjM1IDAgb2JqCjI3MDkKZW5kb2JqCjM2IDAgb2JqCjw8IC9MZW5ndGggMzcgMCBSIC9O - IDMgL0FsdGVybmF0ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0 - cmVhbQp4AYWUTUgUYRjH/7ONBLEG0ZcIxdDBJFQmC1IC0/UrU7Zl1UwJYp19d50cZ6eZ - 3S1FIoTomHWMLlZEh4hO4aFDpzpEBJl1iaCjRRAFXiK2/zuTu2NUvjAzv3me//t8vcMA - VY9SjmNFNGDKzrvJ3ph2enRM2/waVahGFFwpw3M6EokBn6mVz/Vr9S0UaVlqlLHW+zZ8 - q3aZEFA0KndkAz4seTzg45Iv5J08NWckGxOpNNkhN7hDyU7yLfLWbIjHQ5wWngFUtVOT - MxyXcSI7yC1FIytjPiDrdtq0ye+lPe0ZU9Sw38g3OQvauPL9QNseYNOLim3MAx7cA3bX - VWz1NcDOEWDxUMX2PenPR9n1ysscavbDKdEYa/pQKn2vAzbfAH5eL5V+3C6Vft5hDtbx - 1DIKbtHXsjDlJRDUG+xm/OQa/YuDnnxVC7DAOY5sAfqvADc/AvsfAtsfA4lqYKgVkcts - N7jy4iLnAnTmnGnXzE7ktWZdP6J18GiF1mcbTQ1ayrI03+VprvCEWxTpJkxZBc7ZX9t4 - jwp7eJBP9he5JLzu36zMpVNdnCWa2NantOjqJjeQ72fMnj5yPa/3GbdnOGDlgJnvGwo4 - csq24jwXqYnU2OPxk2TGV1QnH5PzkDznFQdlTN9+LnUiQa6lPTmZ65eaXdzbPjMxxDOS - rFgzE53x3/zGLSRl3n3U3HUs/5tnbZFnGIUFARM27zY0JNGLGBrhwEUOGXpMKkxapV/Q - asLD5F+VFhLlXRYVvVjhnhV/z3kUuFvGP4VYHHMN5Qia/k7/oi/rC/pd/fN8baG+4plz - z5rGq2tfGVdmltXIuEGNMr6sKYhvsNoOei1kaZ3iFfTklfWN4eoy9nxt2aPJHOJqfDXU - pQhlasQ448muZfdFssU34edby/av6VH7fPZJTSXXsrp4Zin6fDZcDWv/s6tg0rKr8OSN - kC48a6HuVQ+qfWqL2gpNPaa2q21qF9+OqgPlHcOclYkLrNtl9Sn2YGOa3spJV2aL4N/C - L4b/pV5hC9c0NPkPTbi5jGkJ3xHcNnCHlP/DX7MDDd4KZW5kc3RyZWFtCmVuZG9iagoz - NyAwIG9iago3OTIKZW5kb2JqCjcgMCBvYmoKWyAvSUNDQmFzZWQgMzYgMCBSIF0KZW5k - b2JqCjM4IDAgb2JqCjw8IC9MZW5ndGggMzkgMCBSIC9OIDMgL0FsdGVybmF0ZSAvRGV2 - aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae1XiTvU2xs/YwnZ - 953JPoylsSY72UNIlqwzjG0GM3bZomyhRIgsIZHsW0KSkq0ikiSUJFfRvT9Kda97ht99 - +j2/5/YfdL7Pe87nvO857znfed/n+3kHAFacvo2NJQ0AgEAMJdmaGCAdnZyRdDOAHtAC - Btize2LJwZQ1cMlP2tYzgKCYnqJN7I8a3DcdqHu3Sc2SPdrIp7lelPyTTf+oWXDeZCwA - CDRUeJDg4QBQcUHMhd/DMhTstYe1KJhkb2sI19hAYcLvYpqTFOy1i/f5UnBEaHAoAPRQ - ABc2mETBxRCf9QrY1adS9OFYPNQzSACwjwPr64kDgFUD6tE4AhFixDmIbXEEHAUvQIwh - BIbBe+42ytsyeROP28HRCIoQMAVhwBtEARtgC6wBEhiCIBAIhQSxJZwZwVEG9mpAFSIT - gAEKUJBAHyhBpAQf1E/8q+36dwTy0LMfCIWnIIEZIAIs3EfxGbH7KIBIeNaeXQHaggAB - oGBM//3OPLs+/+2OSgBQ4g/tsBH5ATCg/HY8P3TOvQBkwpgIon/o5L8DoOQFQMM7bBgp - fG8v4p+BGtABRsAO+OBtUUAFvrEVcIX3TwC5oAb0gefgdwQzQhZhhvBDZCAaEJOIb1QS - VNZUMVQ1VDPU+6m1qUnUNdSvaYRonGjyaaZp+Wldaa/Sru5T3he3b4ROgC6Arpeek96f - /h6DMEM0w8z+Q/uLGRGMeMYnTJpM1cw8zGeZt1mILG9Z3Vhn2BzYptgd2Gc43DiWOYM4 - v3GlcfNz1/Lo8kzxEvjo+Sr5DfmXBFIE0YKTQnHCKOFpkRRRTdGPyKoDbmL8YtPilyQc - JYUk30jdkA6XMURxoJZkO+Sy0D7yegrCCt8VXyndPViNOa8crYJXdVAzVdfSwByS1ZQ8 - LKZ1QFtcR1pXQU9NX9/A2tDdKOTIGeNikzbTcbN1C1ZLzNETVvHWNTZTtjR2qva+x0sc - ph3Znaycs1yeuHK5ubhXemx46WCzca991PHZvu/9jQOqCHTEgKCJkMOk6lDusDPhXyKJ - UcsxHqfm4lzj5xOxp1eTyWd2UrLSRNObz5lmLmafusCfcyv35CVEfk2hXREobijxLOMu - H604W2VQjbjeX3umzrKeu2Ghqb4lvs22Q+YW6Jrt7uwt6Ivp9xgwfYB5iBxmHUWMbT3+ - OL7y9O3U0vTyzOrsp7lvC/SveZdklrVX7FcJa2kfr28M//5xk/uzzrb/t8I/h3d2fuXC - r1z4lQu/vgv//134wRtbU/+tG+R+6EA0JI44KL4/4S5/aBPa5ddgyLcUzsMDX8iLFC7E - QoZBgv/lSjScY3b59SBk0D2kvsuc+pCfA6GVwqp7Hsi7M29AhhxLAuGwx8GVYK9O2KMz - QI2A5QWsCAhUMzTGtM10ovQZDF8YcUzjLNqstew8HEmc69wneTx58XyB/CSBCMFYodPC - qSKZoheRhQdKxCrFayQaJC9JxUp7y1iilGUF5ajk3qPH5TsVShVTlIgHj2O0laVU2FS+ - qr5RG1Pv0Cg7lK5JPuyqZaKtpCOgS6P7QW9N/zeD94YrRu+OvDVeMnltumi2YP7KYs7y - 5dEXVjPWqzZfbRnthOzRx7UdrE64OQY5JTifdyk/2eza7/bUfcFjzXMbS4vj8BbxkcOr - +xr52fi7BvgHhhOSiNlBVcG3Q8ZJy+TvYRzhMhE6kXZRftHxMXmnbsT2xT2Ln0h4nDhy - ejDpXnLfme6znSmtqY1pN9NrMqrOlWeWZJ3O9jlvfkExhzPn88WXuX15lZfS8gkFxwpV - Lwtc/qvoZXHXlfySsFK7MqVylvK1q8MV1ZVnqrDX9KtFqr9fn6vpqb1yI67O7aZuvWj9 - TsNCY19TeXNii1erQZtYO1X7YkdfZ+mtuC7X21rdgt1fe2Z6O+/k94XfdehXvUd3b2ag - 7n7cA5tB8cHNh4NDhcOBIzqjbKNvxtoepTw2ekL15M74qYlDE1tPWyaDp+SnVp9VT3s/ - F3s+P1P8wnoWMdv6Ej8nMDf2KmEeM/92oWDRYvGv141vsEs8S71vfZaZl9veua7QrNS9 - t3//fbXiN4vf/li7/MHgwyqMvwKVC3UaTQftEh0HvR4DYX8R4xDTFosEqy1bAvtNjlRO - LJcOrC3+w/OIt4Yvmd9dQFOQW3BDaES4SiRBVFC0HXkMuXYgRUxMrEfcUfwPiSxJlOSA - lLvUV+k8GQOZDdQVWQvZL3LX0HbyCPl6BRdFBsV2JdxB9oN3MCRlSeXnKhmqOqq/q1Wp - O2owavQeCtGU0Hx+OENLR5tGe0gnW/e4nqDeon61QaAhxvCLUe+RZGMzE1aTKdNGs3Rz - bwtdSwHLzaOPrWqtk23cjx2y5bJdtxuyrzye6RBxwsPRzEnZWchln8v6yRmYM3XueR5x - nr5ex7AoHAI3493ok4r38NXwY/V7538nID8wiGBMFCFuBfUHZ4U4kSRJn8g9oWlh9uHI - 8LWIzsjkKOtoweh3MUOn6mKz4oLj7RJUE3kTv55+mdSbXH4m6ax3immqbOp8Wl66eQbI - aDsXkInMfJaVka2fvXW+7oJnDm/O6MWMXPM8+rzBS2fzjQqoC/oLEy7rwozqLo65onnl - S0lLKaFMumyxvPiqQwVbxTDMKt2q7Wst1cTrMtdf16TXKtfO3kiqk6ubvHmqXqJ+rCGs - UbjxfhOxmae5tyWglbd1oC24Xbh9qCO8U6zz8a2YLpmuyduJ3fLdL3oyerV71++U9dne - pb7b1u9zj+/e0EDMffT9Vw+yB/Vh/LWoIqlbaTb2oel86SsZFhiFmByZc1mesDGxm3Ik - c+K5zLjRPCw8n3if8rXzFwjECLoJ6QmLi9CKrIgOIyMPSB14KpYorij+UiJdUkNyWeqi - tL70ukwxyhz1Rfa6nCOaHn1L3k+BX2FIMUoJpfTiYDpGE7OqfFnFXJVFdVwtD8ZdSGP+ - UIWmz2HZwx+0mrRDddR1vun26hXohxhYGEoY/mU0faTJON0EZ6ptxmu2YT5sUWEZe5Rs - 5WftYeNw7KitoZ2mvdJxaQfhE5yO+52A02fnDy6zJx+6trtVul/wiPckeDljTXFq3uI+ - 7D47+DXfF34T/iMBA4HdhDZifdC14NKQAtJ5clro6bDocHJEYGRgFCGaGBN0Kig2OC4k - npRASiSfDk0KTYbF6dmIFNdUwzRUOmv6ZsbsubuZ1VmZ2eTzThd0cyQvMl78lDuV13Wp - JD+pAF9ocVmpiKtou3juSn/JtdL0sqByu6smFZqV8lXIa1zVdNVfr3+oWaydvDFYd/tm - fX15Q25jYVNpc2VLTWt9W0t7Z0dPZ/+twa6x2xPd0z0vexfvfL/L2698z2rA737yg9LB - 2w+fDX0aYRqVHjN85PY46knReM/E/CRiSvyZ8bTv8/SZuhePZjfmOF6pzDssRL7OWapb - frCysPr1A9e64iezP7CbsZ/zt5u+jfz5dmcHALKPMmaXERDM2wDQLUFSgMTABP8fbh7Y - 2dnZghnivrPzJzdACIX/Ddo0yhkKZW5kc3RyZWFtCmVuZG9iagozOSAwIG9iagoyNjM0 - CmVuZG9iagoyNyAwIG9iagpbIC9JQ0NCYXNlZCAzOCAwIFIgXQplbmRvYmoKNDAgMCBv - YmoKPDwgL0xlbmd0aCA0MSAwIFIgL04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VSR0IgL0Zp - bHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7VeJO9TbGz9jCdn3nck+jKWxJjvZ - Q0iWrDOMbQYzdtmibKFEiCwhkexbQpKSrSKSJJQkV9G9P0p1r3uG3336Pb/n9h90vs97 - zue87znvOd953+f7eQcAVpy+jY0lDQCAQAwl2ZoYIB2dnJF0M4Ae0AIG2LN7YsnBlDVw - yU/a1jOAoJieok3sjxrcNx2oe7dJzZI92sinuV6U/JNN/6hZcN5kLAAINFR4kODhAFBx - QcyF38MyFOy1h7UomGRvawjX2EBhwu9impMU7LWL9/lScERocCgA9FAAFzaYRMHFEJ/1 - CtjVp1L04Vg81DNIALCPA+vriQOAVQPq0TgCEWLEOYhtcQQcBS9AjCEEhsF77jbK2zJ5 - E4/bwdEIihAwBWHAG0QBG2ALrAESGIIgEAiFBLElnBnBUQb2akAVIhOAAQpQkEAfKEGk - BB/UT/yr7fp3BPLQsx8IhacggRkgAizcR/EZsfsogEh41p5dAdqCAAGgYEz//c48uz7/ - 7Y5KAFDiD+2wEfkBMKD8djw/dM69AGTCmAiif+jkvwOg5AVAwztsGCl8by/in4Ea0AFG - wA744G1RQAW+sRVwhfdPALmgBvSB5+B3BDNCFmGG8ENkIBoQk4hvVBJU1lQxVDVUM9T7 - qbWpSdQ11K9phGicaPJppmn5aV1pr9Ku7lPeF7dvhE6ALoCul56T3p/+HoMwQzTDzP5D - +4sZEYx4xidMmkzVzDzMZ5m3WYgsb1ndWGfYHNim2B3YZzjcOJY5gzi/caVx83PX8ujy - TPES+Oj5KvkN+ZcEUgTRgpNCccIo4WmRFFFN0Y/IqgNuYvxi0+KXJBwlhSTfSN2QDpcx - RHGglmQ75LLQPvJ6CsIK3xVfKd09WI05rxytgld1UDNV19LAHJLVlDwspnVAW1xHWldB - T01f38Da0N0o5MgZ42KTNtNxs3ULVkvM0RNW8dY1NlO2NHaq9r7HSxymHdmdrJyzXJ64 - crm5uFd6bHjpYLNxr33U8dm+7/2NA6oIdMSAoImQw6TqUO6wM+FfIolRyzEep+biXOPn - E7GnV5PJZ3ZSstJE05vPmWYuZp+6wJ9zK/fkJUR+TaFdEShuKPEs4y4frThbZVCNuN5f - e6bOsp67YaGpviW+zbZD5hbomu3u7C3oi+n3GDB9gHmIHGYdRYxtPf44vvL07dTS9PLM - 6uynuW8L9K95l2SWtVfsVwlraR+vbwz//nGT+7POtv+3wj+Hd3Z+5cKvXPiVC7++C/// - XfjBG1tT/60b5H7oQDQkjjgovj/hLn9oE9rl12DItxTOwwNfyIsULsRChkGC/+VKNJxj - dvn1IGTQPaS+y5z6kJ8DoZXCqnseyLszb0CGHEsC4bDHwZVgr07YozNAjYDlBawICFQz - NMa0zXSi9BkMXxhxTOMs2qy17DwcSZzr3Cd5PHnxfIH8JIEIwVih08KpIpmiF5GFB0rE - KsVrJBokL0nFSnvLWKKUZQXlqOTeo8flOxVKFVOUiAePY7SVpVTYVL6qvlEbU+/QKDuU - rkk+7Kploq2kI6BLo/tBb03/N4P3hitG7468NV4yeW26aLZg/spizvLl0RdWM9arNl9t - Ge2E7NHHtR2sTrg5BjklOJ93KT/Z7Nrv9tR9wWPNcxtLi+PwFvGRw6v7GvnZ+LsG+AeG - E5KI2UFVwbdDxknL5O9hHOEyETqRdlF+0fExeaduxPbFPYufSHicOHJ6MOlect+Z7rOd - Ka2pjWk302syqs6VZ5Zknc72OW9+QTGHM+fzxZe5fXmVl9LyCQXHClUvC1z+q+hlcdeV - /JKwUrsypXKW8rWrwxXVlWeqsNf0q0Wqv1+fq+mpvXIjrs7tpm69aP1Ow0JjX1N5c2KL - V6tBm1g7VftiR19n6a24LtfbWt2C3V97Zno77+T3hd916Fe9R3dvZqDuftwDm0Hxwc2H - g0OFw4EjOqNso2/G2h6lPDZ6QvXkzvipiUMTW09bJoOn5KdWn1VPez8Xez4/U/zCehYx - 2/oSPycwN/YqYR4z/3ahYNFi8a/XjW+wSzxLvW99lpmX2965rtCs1L23f/99teI3i9/+ - WLv8weDDKoy/ApULdRpNB+0SHQe9HgNhfxHjENMWiwSrLVsC+02OVE4slw6sLf7D84i3 - hi+Z311AU5BbcENoRLhKJEFUULQdeQy5diBFTEysR9xR/A+JLEmU5ICUu9RX6TwZA5kN - 1BVZC9kvctfQdvII+XoFF0UGxXYl3EH2g3cwJGVJ5ecqGao6qr+rVak7ajBq9B4K0ZTQ - fH44Q0tHm0Z7SCdb97ieoN6ifrVBoCHG8ItR75FkYzMTVpMp00azdHNvC11LAcvNo4+t - aq2TbdyPHbLlsl23G7KvPJ7pEHHCw9HMSdlZyGWfy/rJGZgzde55HnGevl7HsCgcAjfj - 3eiTivfw1fBj9XvnfycgPzCIYEwUIW4F9QdnhTiRJEmfyD2haWH24cjwtYjOyOQo62jB - 6HcxQ6fqYrPiguPtElQTeRO/nn6Z1JtcfibprHeKaaps6nxaXrp5BshoOxeQicx8lpWR - rZ+9db7ugmcOb87oxYxc8zz6vMFLZ/ONCqgL+gsTLuvCjOoujrmieeVLSUspoUy6bLG8 - +KpDBVvFMMwq3artay3VxOsy11/XpNcq187eSKqTq5u8eapeon6sIaxRuPF+E7GZp7m3 - JaCVt3WgLbhduH2oI7xTrPPxrZguma7J24nd8t0vejJ6tXvX75T12d6lvtvW73OP797Q - QMx99P1XD7IH9WH8tagiqVtpNvah6XzpKxkWGIWYHJlzWZ6wMbGbciRz4rnMuNE8LDyf - eJ/ytfMXCMQIugnpCYuL0IqsiA4jIw9IHXgqliiuKP5SIl1SQ3JZ6qK0vvS6TDHKHPVF - 9rqcI5oefUveT4FfYUgxSgml9OJgOkYTs6p8WcVclUV1XC0Pxl1IY/5QhabPYdnDH7Sa - tEN11HW+6fbqFeiHGFgYShj+ZTR9pMk43QRnqm3Ga7ZhPmxRYRl7lGzlZ+1h43DsqK2h - naa90nFpB+ETnI77nYDTZ+cPLrMnH7q2u1W6X/CI9yR4OWNNcWre4j7sPjv4Nd8XfhP+ - IwEDgd2ENmJ90LXg0pAC0nlyWujpsOhwckRgZGAUIZoYE3QqKDY4LiSelEBKJJ8OTQpN - hsXp2YgU11TDNFQ6a/pmxuy5u5nVWZnZ5PNOF3RzJC8yXvyUO5XXdakkP6kAX2hxWamI - q2i7eO5Kf8m10vSyoHK7qyYVmpXyVchrXNV01V+vf6hZrJ28MVh3+2Z9fXlDbmNhU2lz - ZUtNa31bS3tnR09n/63BrrHbE93TPS97F+98v8vbr3zPasDvfvKD0sHbD58NfRphGpUe - M3zk9jjqSdF4z8T8JGJK/JnxtO/z9Jm6F49mN+Y4XqnMOyxEvs5Zqlt+sLKw+vUD17ri - J7M/sJuxn/O3m76N/Pl2ZwcAso8yZpcREMzbANAtQVKAxMAE/x9uHtjZ2dmCGeK+s/Mn - N0AIhf8N2jTKGQplbmRzdHJlYW0KZW5kb2JqCjQxIDAgb2JqCjI2MzQKZW5kb2JqCjI0 - IDAgb2JqClsgL0lDQ0Jhc2VkIDQwIDAgUiBdCmVuZG9iago0MiAwIG9iago8PCAvTGVu - Z3RoIDQzIDAgUiAvTiAzIC9BbHRlcm5hdGUgL0RldmljZVJHQiAvRmlsdGVyIC9GbGF0 - ZURlY29kZSA+PgpzdHJlYW0KeAHtV4k71NsbP2MJ2fedyT6MpbEmO9lDSJasM4xtBjN2 - 2aJsoUSILCGR7FtCkpKtIpIklCRX0b0/SnWve4bfffo9v+f2H3S+z3vO57zvOe8533nf - 5/t5BwBWnL6NjSUNAIBADCXZmhggHZ2ckXQzgB7QAgbYs3tiycGUNXDJT9rWM4CgmJ6i - TeyPGtw3Hah7t0nNkj3ayKe5XpT8k03/qFlw3mQsAAg0VHiQ4OEAUHFBzIXfwzIU7LWH - tSiYZG9rCNfYQGHC72KakxTstYv3+VJwRGhwKAD0UAAXNphEwcUQn/UK2NWnUvThWDzU - M0gAsI8D6+uJA4BVA+rROAIRYsQ5iG1xBBwFL0CMIQSGwXvuNsrbMnkTj9vB0QiKEDAF - YcAbRAEbYAusARIYgiAQCIUEsSWcGcFRBvZqQBUiE4ABClCQQB8oQaQEH9RP/Kvt+ncE - 8tCzHwiFpyCBGSACLNxH8Rmx+yiASHjWnl0B2oIAAaBgTP/9zjy7Pv/tjkoAUOIP7bAR - +QEwoPx2PD90zr0AZMKYCKJ/6OS/A6DkBUDDO2wYKXxvL+KfgRrQAUbADvjgbVFABb6x - FXCF908AuaAG9IHn4HcEM0IWYYbwQ2QgGhCTiG9UElTWVDFUNVQz1PuptalJ1DXUr2mE - aJxo8mmmaflpXWmv0q7uU94Xt2+EToAugK6XnpPen/4egzBDNMPM/kP7ixkRjHjGJ0ya - TNXMPMxnmbdZiCxvWd1YZ9gc2KbYHdhnONw4ljmDOL9xpXHzc9fy6PJM8RL46Pkq+Q35 - lwRSBNGCk0JxwijhaZEUUU3Rj8iqA25i/GLT4pckHCWFJN9I3ZAOlzFEcaCWZDvkstA+ - 8noKwgrfFV8p3T1YjTmvHK2CV3VQM1XX0sAcktWUPCymdUBbXEdaV0FPTV/fwNrQ3Sjk - yBnjYpM203GzdQtWS8zRE1bx1jU2U7Y0dqr2vsdLHKYd2Z2snLNcnrhyubm4V3pseOlg - s3GvfdTx2b7v/Y0Dqgh0xICgiZDDpOpQ7rAz4V8iiVHLMR6n5uJc4+cTsadXk8lndlKy - 0kTTm8+ZZi5mn7rAn3Mr9+QlRH5NoV0RKG4o8SzjLh+tOFtlUI243l97ps6ynrthoam+ - Jb7NtkPmFuia7e7sLeiL6fcYMH2AeYgcZh1FjG09/ji+8vTt1NL08szq7Ke5bwv0r3mX - ZJa1V+xXCWtpH69vDP/+cZP7s862/7fCP4d3dn7lwq9c+JULv74L//9d+MEbW1P/rRvk - fuhANCSOOCi+P+Euf2gT2uXXYMi3FM7DA1/IixQuxEKGQYL/5Uo0nGN2+fUgZNA9pL7L - nPqQnwOhlcKqex7IuzNvQIYcSwLhsMfBlWCvTtijM0CNgOUFrAgIVDM0xrTNdKL0GQxf - GHFM4yzarLXsPBxJnOvcJ3k8efF8gfwkgQjBWKHTwqkimaIXkYUHSsQqxWskGiQvScVK - e8tYopRlBeWo5N6jx+U7FUoVU5SIB49jtJWlVNhUvqq+URtT79AoO5SuST7sqmWiraQj - oEuj+0FvTf83g/eGK0bvjrw1XjJ5bbpotmD+ymLO8uXRF1Yz1qs2X20Z7YTs0ce1HaxO - uDkGOSU4n3cpP9ns2u/21H3BY81zG0uL4/AW8ZHDq/sa+dn4uwb4B4YTkojZQVXBt0PG - Scvk72Ec4TIROpF2UX7R8TF5p27E9sU9i59IeJw4cnow6V5y35nus50pramNaTfTazKq - zpVnlmSdzvY5b35BMYcz5/PFl7l9eZWX0vIJBccKVS8LXP6r6GVx15X8krBSuzKlcpby - tavDFdWVZ6qw1/SrRaq/X5+r6am9ciOuzu2mbr1o/U7DQmNfU3lzYotXq0GbWDtV+2JH - X2fprbgu19ta3YLdX3tmejvv5PeF33XoV71Hd29moO5+3AObQfHBzYeDQ4XDgSM6o2yj - b8baHqU8NnpC9eTO+KmJQxNbT1smg6fkp1afVU97Pxd7Pj9T/MJ6FjHb+hI/JzA39iph - HjP/dqFg0WLxr9eNb7BLPEu9b32WmZfb3rmu0KzUvbd//3214jeL3/5Yu/zB4MMqjL8C - lQt1Gk0H7RIdB70eA2F/EeMQ0xaLBKstWwL7TY5UTiyXDqwt/sPziLeGL5nfXUBTkFtw - Q2hEuEokQVRQtB15DLl2IEVMTKxH3FH8D4ksSZTkgJS71FfpPBkDmQ3UFVkL2S9y19B2 - 8gj5egUXRQbFdiXcQfaDdzAkZUnl5yoZqjqqv6tVqTtqMGr0HgrRlNB8fjhDS0ebRntI - J1v3uJ6g3qJ+tUGgIcbwi1HvkWRjMxNWkynTRrN0c28LXUsBy82jj61qrZNt3I8dsuWy - Xbcbsq88nukQccLD0cxJ2VnIZZ/L+skZmDN17nkecZ6+XsewKBwCN+Pd6JOK9/DV8GP1 - e+d/JyA/MIhgTBQhbgX1B2eFOJEkSZ/IPaFpYfbhyPC1iM7I5CjraMHodzFDp+pis+KC - 4+0SVBN5E7+efpnUm1x+Jumsd4ppqmzqfFpeunkGyGg7F5CJzHyWlZGtn711vu6CZw5v - zujFjFzzPPq8wUtn840KqAv6CxMu68KM6i6OuaJ55UtJSymhTLpssbz4qkMFW8UwzCrd - qu1rLdXE6zLXX9ek1yrXzt5IqpOrm7x5ql6ifqwhrFG48X4TsZmnubcloJW3daAtuF24 - fagjvFOs8/GtmC6Zrsnbid3y3S96Mnq1e9fvlPXZ3qW+29bvc4/v3tBAzH30/VcPsgf1 - Yfy1qCKpW2k29qHpfOkrGRYYhZgcmXNZnrAxsZtyJHPiucy40TwsPJ94n/K18xcIxAi6 - CekJi4vQiqyIDiMjD0gdeCqWKK4o/lIiXVJDclnqorS+9LpMMcoc9UX2upwjmh59S95P - gV9hSDFKCaX04mA6RhOzqnxZxVyVRXVcLQ/GXUhj/lCFps9h2cMftJq0Q3XUdb7p9uoV - 6IcYWBhKGP5lNH2kyTjdBGeqbcZrtmE+bFFhGXuUbOVn7WHjcOyoraGdpr3ScWkH4ROc - jvudgNNn5w8usycfura7Vbpf8Ij3JHg5Y01xat7iPuw+O/g13xd+E/4jAQOB3YQ2Yn3Q - teDSkALSeXJa6Omw6HByRGBkYBQhmhgTdCooNjguJJ6UQEoknw5NCk2GxenZiBTXVMM0 - VDpr+mbG7Lm7mdVZmdnk804XdHMkLzJe/JQ7ldd1qSQ/qQBfaHFZqYiraLt47kp/ybXS - 9LKgcrurJhWalfJVyGtc1XTVX69/qFmsnbwxWHf7Zn19eUNuY2FTaXNlS01rfVtLe2dH - T2f/rcGusdsT3dM9L3sX73y/y9uvfM9qwO9+8oPSwdsPnw19GmEalR4zfOT2OOpJ0XjP - xPwkYkr8mfG07/P0mboXj2Y35jheqcw7LES+zlmqW36wsrD69QPXuuInsz+wm7Gf87eb - vo38+XZnBwCyjzJmlxEQzNsA0C1BUoDEwAT/H24e2NnZ2YIZ4r6z8yc3QAiF/w3aNMoZ - CmVuZHN0cmVhbQplbmRvYmoKNDMgMCBvYmoKMjYzNAplbmRvYmoKMjEgMCBvYmoKWyAv - SUNDQmFzZWQgNDIgMCBSIF0KZW5kb2JqCjQ0IDAgb2JqCjw8IC9MZW5ndGggNDUgMCBS - IC9OIDEgL0FsdGVybmF0ZSAvRGV2aWNlR3JheSAvRmlsdGVyIC9GbGF0ZURlY29kZSA+ - PgpzdHJlYW0KeAGFUk9IFFEc/s02EoSIQYV4iHcKCZUprKyg2nZ1WZVtW5XSohhn37qj - szPTm9k1xZMEXaI8dQ+iY3Ts0KGbl6LArEvXIKkgCDx16PvN7OoohG95O9/7/f1+33tE - bZ2m7zspQVRzQ5UrpaduTk2Lgx8pRR3UTlimFfjpYnGMseu5kr+719Zn0tiy3se1dvv2 - PbWVZWAh6i22txD6IZFmAB+ZnyhlgLPAHZav2D4BPFgOrBrwI6IDD5q5MNPRnHSlsi2R - U+aiKCqvYjtJrvv5uca+i7WJg/5cj2bWjr2z6qrRTNS090ShvA+uRBnPX1T2bDUUpw3j - nEhDGinyrtXfK0zHEZErEEoGUjVkuZ9qTp114HUYu126k+P49hClPslgqIm16bKZHYV9 - AHYqy+wQ8AXo8bJiD+eBe2H/W1HDk8AnYT9kh3nWrR/2F65T4HuEPTXgzhSuxfHaih9e - LQFD91QjaIxzTcTT1zlzpIjvMdQZmPdGOaYLMXeWqhM3gDthH1mqZgqxXfuu6iXuewJ3 - 0+M70Zs5C1ygHElysRXZFNA8CVgUfYuwSQ48Ps4eVeB3qJjAHLmJ3M0o9x7VERtno1KB - VnqNV8ZP47nxxfhlbBjPgH6sdtd7fP/p4xV117Y+PPmNetw5rr2dG1VhVnFlC93/xzKE - j9knOabB06FZWGvYduQPmsxMsAwoxH8FPpf6khNV3NXu7bhFEsxQPixsJbpLVG4p1Oo9 - g0qsHCvYAHZwksQsWhy4U2u6OXh32CJ6bflNV7Lrhv769nr72vIebcqoKSgTzbNEZpSx - W6Pk3Xjb/WaREZ84Or7nvYpayf5JRRA/hTlaKvIUVfRWUNbEb2cOfhu2flw/pef1Qf08 - CT2tn9Gv6KMRvgx0Sc/Cc1Efo0nwsGkh4hKgioMz1E5UY40D4inx8rRbZJH9D0AZ/WYK - ZW5kc3RyZWFtCmVuZG9iago0NSAwIG9iago3MDQKZW5kb2JqCjE4IDAgb2JqClsgL0lD - Q0Jhc2VkIDQ0IDAgUiBdCmVuZG9iago0NiAwIG9iago8PCAvTGVuZ3RoIDQ3IDAgUiAv - TiAzIC9BbHRlcm5hdGUgL0RldmljZVJHQiAvRmlsdGVyIC9GbGF0ZURlY29kZSA+Pgpz - dHJlYW0KeAHtV4k71NsbP2MJ2fedyT6MpbEmO9lDSJasM4xtBjN22aJsoUSILCGR7FtC - kpKtIpIklCRX0b0/SnWve4bfffo9v+f2H3S+z3vO57zvOe8533nf5/t5BwBWnL6NjSUN - AIBADCXZmhggHZ2ckXQzgB7QAgbYs3tiycGUNXDJT9rWM4CgmJ6iTeyPGtw3Hah7t0nN - kj3ayKe5XpT8k03/qFlw3mQsAAg0VHiQ4OEAUHFBzIXfwzIU7LWHtSiYZG9rCNfYQGHC - 72KakxTstYv3+VJwRGhwKAD0UAAXNphEwcUQn/UK2NWnUvThWDzUM0gAsI8D6+uJA4BV - A+rROAIRYsQ5iG1xBBwFL0CMIQSGwXvuNsrbMnkTj9vB0QiKEDAFYcAbRAEbYAusARIY - giAQCIUEsSWcGcFRBvZqQBUiE4ABClCQQB8oQaQEH9RP/Kvt+ncE8tCzHwiFpyCBGSAC - LNxH8Rmx+yiASHjWnl0B2oIAAaBgTP/9zjy7Pv/tjkoAUOIP7bAR+QEwoPx2PD90zr0A - ZMKYCKJ/6OS/A6DkBUDDO2wYKXxvL+KfgRrQAUbADvjgbVFABb6xFXCF908AuaAG9IHn - 4HcEM0IWYYbwQ2QgGhCTiG9UElTWVDFUNVQz1PuptalJ1DXUr2mEaJxo8mmmaflpXWmv - 0q7uU94Xt2+EToAugK6XnpPen/4egzBDNMPM/kP7ixkRjHjGJ0yaTNXMPMxnmbdZiCxv - Wd1YZ9gc2KbYHdhnONw4ljmDOL9xpXHzc9fy6PJM8RL46Pkq+Q35lwRSBNGCk0Jxwijh - aZEUUU3Rj8iqA25i/GLT4pckHCWFJN9I3ZAOlzFEcaCWZDvkstA+8noKwgrfFV8p3T1Y - jTmvHK2CV3VQM1XX0sAcktWUPCymdUBbXEdaV0FPTV/fwNrQ3SjkyBnjYpM203GzdQtW - S8zRE1bx1jU2U7Y0dqr2vsdLHKYd2Z2snLNcnrhyubm4V3pseOlgs3GvfdTx2b7v/Y0D - qgh0xICgiZDDpOpQ7rAz4V8iiVHLMR6n5uJc4+cTsadXk8lndlKy0kTTm8+ZZi5mn7rA - n3Mr9+QlRH5NoV0RKG4o8SzjLh+tOFtlUI243l97ps6ynrthoam+Jb7NtkPmFuia7e7s - LeiL6fcYMH2AeYgcZh1FjG09/ji+8vTt1NL08szq7Ke5bwv0r3mXZJa1V+xXCWtpH69v - DP/+cZP7s862/7fCP4d3dn7lwq9c+JULv74L//9d+MEbW1P/rRvkfuhANCSOOCi+P+Eu - f2gT2uXXYMi3FM7DA1/IixQuxEKGQYL/5Uo0nGN2+fUgZNA9pL7LnPqQnwOhlcKqex7I - uzNvQIYcSwLhsMfBlWCvTtijM0CNgOUFrAgIVDM0xrTNdKL0GQxfGHFM4yzarLXsPBxJ - nOvcJ3k8efF8gfwkgQjBWKHTwqkimaIXkYUHSsQqxWskGiQvScVKe8tYopRlBeWo5N6j - x+U7FUoVU5SIB49jtJWlVNhUvqq+URtT79AoO5SuST7sqmWiraQjoEuj+0FvTf83g/eG - K0bvjrw1XjJ5bbpotmD+ymLO8uXRF1Yz1qs2X20Z7YTs0ce1HaxOuDkGOSU4n3cpP9ns - 2u/21H3BY81zG0uL4/AW8ZHDq/sa+dn4uwb4B4YTkojZQVXBt0PGScvk72Ec4TIROpF2 - UX7R8TF5p27E9sU9i59IeJw4cnow6V5y35nus50pramNaTfTazKqzpVnlmSdzvY5b35B - MYcz5/PFl7l9eZWX0vIJBccKVS8LXP6r6GVx15X8krBSuzKlcpbytavDFdWVZ6qw1/Sr - Raq/X5+r6am9ciOuzu2mbr1o/U7DQmNfU3lzYotXq0GbWDtV+2JHX2fprbgu19ta3YLd - X3tmejvv5PeF33XoV71Hd29moO5+3AObQfHBzYeDQ4XDgSM6o2yjb8baHqU8NnpC9eTO - +KmJQxNbT1smg6fkp1afVU97Pxd7Pj9T/MJ6FjHb+hI/JzA39iphHjP/dqFg0WLxr9eN - b7BLPEu9b32WmZfb3rmu0KzUvbd//3214jeL3/5Yu/zB4MMqjL8ClQt1Gk0H7RIdB70e - A2F/EeMQ0xaLBKstWwL7TY5UTiyXDqwt/sPziLeGL5nfXUBTkFtwQ2hEuEokQVRQtB15 - DLl2IEVMTKxH3FH8D4ksSZTkgJS71FfpPBkDmQ3UFVkL2S9y19B28gj5egUXRQbFdiXc - QfaDdzAkZUnl5yoZqjqqv6tVqTtqMGr0HgrRlNB8fjhDS0ebRntIJ1v3uJ6g3qJ+tUGg - Icbwi1HvkWRjMxNWkynTRrN0c28LXUsBy82jj61qrZNt3I8dsuWyXbcbsq88nukQccLD - 0cxJ2VnIZZ/L+skZmDN17nkecZ6+XsewKBwCN+Pd6JOK9/DV8GP1e+d/JyA/MIhgTBQh - bgX1B2eFOJEkSZ/IPaFpYfbhyPC1iM7I5CjraMHodzFDp+pis+KC4+0SVBN5E7+efpnU - m1x+Jumsd4ppqmzqfFpeunkGyGg7F5CJzHyWlZGtn711vu6CZw5vzujFjFzzPPq8wUtn - 840KqAv6CxMu68KM6i6OuaJ55UtJSymhTLpssbz4qkMFW8UwzCrdqu1rLdXE6zLXX9ek - 1yrXzt5IqpOrm7x5ql6ifqwhrFG48X4TsZmnubcloJW3daAtuF24fagjvFOs8/GtmC6Z - rsnbid3y3S96Mnq1e9fvlPXZ3qW+29bvc4/v3tBAzH30/VcPsgf1Yfy1qCKpW2k29qHp - fOkrGRYYhZgcmXNZnrAxsZtyJHPiucy40TwsPJ94n/K18xcIxAi6CekJi4vQiqyIDiMj - D0gdeCqWKK4o/lIiXVJDclnqorS+9LpMMcoc9UX2upwjmh59S95PgV9hSDFKCaX04mA6 - RhOzqnxZxVyVRXVcLQ/GXUhj/lCFps9h2cMftJq0Q3XUdb7p9uoV6IcYWBhKGP5lNH2k - yTjdBGeqbcZrtmE+bFFhGXuUbOVn7WHjcOyoraGdpr3ScWkH4ROcjvudgNNn5w8usycf - ura7Vbpf8Ij3JHg5Y01xat7iPuw+O/g13xd+E/4jAQOB3YQ2Yn3QteDSkALSeXJa6Omw - 6HByRGBkYBQhmhgTdCooNjguJJ6UQEoknw5NCk2GxenZiBTXVMM0VDpr+mbG7Lm7mdVZ - mdnk804XdHMkLzJe/JQ7ldd1qSQ/qQBfaHFZqYiraLt47kp/ybXS9LKgcrurJhWalfJV - yGtc1XTVX69/qFmsnbwxWHf7Zn19eUNuY2FTaXNlS01rfVtLe2dHT2f/rcGusdsT3dM9 - L3sX73y/y9uvfM9qwO9+8oPSwdsPnw19GmEalR4zfOT2OOpJ0XjPxPwkYkr8mfG07/P0 - mboXj2Y35jheqcw7LES+zlmqW36wsrD69QPXuuInsz+wm7Gf87ebvo38+XZnBwCyjzJm - lxEQzNsA0C1BUoDEwAT/H24e2NnZ2YIZ4r6z8yc3QAiF/w3aNMoZCmVuZHN0cmVhbQpl - bmRvYmoKNDcgMCBvYmoKMjYzNAplbmRvYmoKMzMgMCBvYmoKWyAvSUNDQmFzZWQgNDYg - MCBSIF0KZW5kb2JqCjQ4IDAgb2JqCjw8IC9MZW5ndGggNDkgMCBSIC9OIDMgL0FsdGVy - bmF0ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae1X - iTvU2xs/YwnZ953JPoylsSY72UNIlqwzjG0GM3bZomyhRIgsIZHsW0KSkq0ikiSUJFfR - vT9Kda97ht99+j2/5/YfdL7Pe87nvO857znfed/n+3kHAFacvo2NJQ0AgEAMJdmaGCAd - nZyRdDOAHtACBtize2LJwZQ1cMlP2tYzgKCYnqJN7I8a3DcdqHu3Sc2SPdrIp7lelPyT - Tf+oWXDeZCwACDRUeJDg4QBQcUHMhd/DMhTstYe1KJhkb2sI19hAYcLvYpqTFOy1i/f5 - UnBEaHAoAPRQABc2mETBxRCf9QrY1adS9OFYPNQzSACwjwPr64kDgFUD6tE4AhFixDmI - bXEEHAUvQIwhBIbBe+42ytsyeROP28HRCIoQMAVhwBtEARtgC6wBEhiCIBAIhQSxJZwZ - wVEG9mpAFSITgAEKUJBAHyhBpAQf1E/8q+36dwTy0LMfCIWnIIEZIAIs3EfxGbH7KIBI - eNaeXQHaggABoGBM//3OPLs+/+2OSgBQ4g/tsBH5ATCg/HY8P3TOvQBkwpgIon/o5L8D - oOQFQMM7bBgpfG8v4p+BGtABRsAO+OBtUUAFvrEVcIX3TwC5oAb0gefgdwQzQhZhhvBD - ZCAaEJOIb1QSVNZUMVQ1VDPU+6m1qUnUNdSvaYRonGjyaaZp+Wldaa/Sru5T3he3b4RO - gC6Arpeek96f/h6DMEM0w8z+Q/uLGRGMeMYnTJpM1cw8zGeZt1mILG9Z3Vhn2BzYptgd - 2Gc43DiWOYM4v3GlcfNz1/Lo8kzxEvjo+Sr5DfmXBFIE0YKTQnHCKOFpkRRRTdGPyKoD - bmL8YtPilyQcJYUk30jdkA6XMURxoJZkO+Sy0D7yegrCCt8VXyndPViNOa8crYJXdVAz - VdfSwByS1ZQ8LKZ1QFtcR1pXQU9NX9/A2tDdKOTIGeNikzbTcbN1C1ZLzNETVvHWNTZT - tjR2qva+x0scph3Znaycs1yeuHK5ubhXemx46WCzca991PHZvu/9jQOqCHTEgKCJkMOk - 6lDusDPhXyKJUcsxHqfm4lzj5xOxp1eTyWd2UrLSRNObz5lmLmafusCfcyv35CVEfk2h - XREobijxLOMuH604W2VQjbjeX3umzrKeu2Ghqb4lvs22Q+YW6Jrt7uwt6Ivp9xgwfYB5 - iBxmHUWMbT3+OL7y9O3U0vTyzOrsp7lvC/SveZdklrVX7FcJa2kfr28M//5xk/uzzrb/ - t8I/h3d2fuXCr1z4lQu/vgv//134wRtbU/+tG+R+6EA0JI44KL4/4S5/aBPa5ddgyLcU - zsMDX8iLFC7EQoZBgv/lSjScY3b59SBk0D2kvsuc+pCfA6GVwqp7Hsi7M29AhhxLAuGw - x8GVYK9O2KMzQI2A5QWsCAhUMzTGtM10ovQZDF8YcUzjLNqstew8HEmc69wneTx58XyB - /CSBCMFYodPCqSKZoheRhQdKxCrFayQaJC9JxUp7y1iilGUF5ajk3qPH5TsVShVTlIgH - j2O0laVU2FS+qr5RG1Pv0Cg7lK5JPuyqZaKtpCOgS6P7QW9N/zeD94YrRu+OvDVeMnlt - umi2YP7KYs7y5dEXVjPWqzZfbRnthOzRx7UdrE64OQY5JTifdyk/2eza7/bUfcFjzXMb - S4vj8BbxkcOr+xr52fi7BvgHhhOSiNlBVcG3Q8ZJy+TvYRzhMhE6kXZRftHxMXmnbsT2 - xT2Ln0h4nDhyejDpXnLfme6znSmtqY1pN9NrMqrOlWeWZJ3O9jlvfkExhzPn88WXuX15 - lZfS8gkFxwpVLwtc/qvoZXHXlfySsFK7MqVylvK1q8MV1ZVnqrDX9KtFqr9fn6vpqb1y - I67O7aZuvWj9TsNCY19TeXNii1erQZtYO1X7YkdfZ+mtuC7X21rdgt1fe2Z6O+/k94Xf - dehXvUd3b2ag7n7cA5tB8cHNh4NDhcOBIzqjbKNvxtoepTw2ekL15M74qYlDE1tPWyaD - p+SnVp9VT3s/F3s+P1P8wnoWMdv6Ej8nMDf2KmEeM/92oWDRYvGv141vsEs8S71vfZaZ - l9veua7QrNS9t3//fbXiN4vf/li7/MHgwyqMvwKVC3UaTQftEh0HvR4DYX8R4xDTFosE - qy1bAvtNjlROLJcOrC3+w/OIt4Yvmd9dQFOQW3BDaES4SiRBVFC0HXkMuXYgRUxMrEfc - UfwPiSxJlOSAlLvUV+k8GQOZDdQVWQvZL3LX0HbyCPl6BRdFBsV2JdxB9oN3MCRlSeXn - KhmqOqq/q1WpO2owavQeCtGU0Hx+OENLR5tGe0gnW/e4nqDeon61QaAhxvCLUe+RZGMz - E1aTKdNGs3RzbwtdSwHLzaOPrWqtk23cjx2y5bJdtxuyrzye6RBxwsPRzEnZWchln8v6 - yRmYM3XueR5xnr5ex7AoHAI3493ok4r38NXwY/V7538nID8wiGBMFCFuBfUHZ4U4kSRJ - n8g9oWlh9uHI8LWIzsjkKOtoweh3MUOn6mKz4oLj7RJUE3kTv55+mdSbXH4m6ax3immq - bOp8Wl66eQbIaDsXkInMfJaVka2fvXW+7oJnDm/O6MWMXPM8+rzBS2fzjQqoC/oLEy7r - wozqLo65onnlS0lLKaFMumyxvPiqQwVbxTDMKt2q7Wst1cTrMtdf16TXKtfO3kiqk6ub - vHmqXqJ+rCGsUbjxfhOxmae5tyWglbd1oC24Xbh9qCO8U6zz8a2YLpmuyduJ3fLdL3oy - erV71++U9dnepb7b1u9zj+/e0EDMffT9Vw+yB/Vh/LWoIqlbaTb2oel86SsZFhiFmByZ - c1mesDGxm3Ikc+K5zLjRPCw8n3if8rXzFwjECLoJ6QmLi9CKrIgOIyMPSB14KpYorij+ - UiJdUkNyWeqitL70ukwxyhz1Rfa6nCOaHn1L3k+BX2FIMUoJpfTiYDpGE7OqfFnFXJVF - dVwtD8ZdSGP+UIWmz2HZwx+0mrRDddR1vun26hXohxhYGEoY/mU0faTJON0EZ6ptxmu2 - YT5sUWEZe5Rs5WftYeNw7KitoZ2mvdJxaQfhE5yO+52A02fnDy6zJx+6trtVul/wiPck - eDljTXFq3uI+7D47+DXfF34T/iMBA4HdhDZifdC14NKQAtJ5clro6bDocHJEYGRgFCGa - GBN0Kig2OC4knpRASiSfDk0KTYbF6dmIFNdUwzRUOmv6ZsbsubuZ1VmZ2eTzThd0cyQv - Ml78lDuV13WpJD+pAF9ocVmpiKtou3juSn/JtdL0sqByu6smFZqV8lXIa1zVdNVfr3+o - WaydvDFYd/tmfX15Q25jYVNpc2VLTWt9W0t7Z0dPZ/+twa6x2xPd0z0vexfvfL/L2698 - z2rA737yg9LB2w+fDX0aYRqVHjN85PY46knReM/E/CRiSvyZ8bTv8/SZuhePZjfmOF6p - zDssRL7OWapbfrCysPr1A9e64iezP7CbsZ/zt5u+jfz5dmcHALKPMmaXERDM2wDQLUFS - gMTABP8fbh7Y2dnZghnivrPzJzdACIX/Ddo0yhkKZW5kc3RyZWFtCmVuZG9iago0OSAw - IG9iagoyNjM0CmVuZG9iagozMCAwIG9iagpbIC9JQ0NCYXNlZCA0OCAwIFIgXQplbmRv - YmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZXMgL01lZGlhQm94IFswIDAgNzU2IDU1M10g - L0NvdW50IDEgL0tpZHMgWyAyIDAgUiBdID4+CmVuZG9iago1MCAwIG9iago8PCAvVHlw - ZSAvQ2F0YWxvZyAvUGFnZXMgMyAwIFIgL1ZlcnNpb24gLzEuNCA+PgplbmRvYmoKNTEg - MCBvYmoKPDwgL0xlbmd0aCA1MiAwIFIgL0xlbmd0aDEgNzE3MiAvRmlsdGVyIC9GbGF0 - ZURlY29kZSA+PgpzdHJlYW0KeAG9WXtYlNW6f9f3fXNBUG4Kw3Vm+BiugwgooJiMOMNF - TFEUGfMygCCSGClSlhK7dJd4OVtNbWtHs4s7JXME0gG2Ru7c6a6dVrub2043szpPPtU5 - dWqHzJzf+mYk7dn1+EdPfM87613X97d+77vW960FMSIKoDYSyVLTWNVEa2gAJa9AWmta - mg2bP5+0l4hNIxKX1TUtaQz+4C9/I5JcRMMClixbXff11rzpRCNeJNIa6murFn+jX9ZN - FHYJ/bPrURAwMOIOovBo5OPrG5vvtm1UP4O8BXnLsjtqqkJygxzItyEf11h1d5N25bDv - kX8SecPyqsbahPzouchjfEppumNlM3tGqEP+K+QLmlbUNv35geUZRLqxwHcOZQwP/wsg - Na1AaqOxvhKlWPkRflSv08TrdK8qIVFB1BANRAsh8qNh5I/xhys5TN2XBqJxPwWpTlCS - qo0ipXTSE3nehVzgqXuO57LqJQpyN3q+FvPQp4eL4M6fSP20mfbQEdh5GnoSLaRH6Cxr - oB42n7rpLRZLo6mNJHLRNHqFeTyvUR09ifbNdIp20FFgSaJGGoXaLczkuQd5C/RqWud5 - nOIpl35PJ2g8Rt1CVzwHPV2onUVz6BB1oP/LTBaOSqGeZz2XML+ZGHMdal7zTPMcoRAy - UwGVoXQdnWQm8YKnnnSUB3SP0j7aTy/QF+x+1u2p97R4zns+JAG10VSOZy3rZh+KR6Tf - ex71/LfHDSaSKAVWHbSdnsD4R/D0w1U2djtrZtvZDsEi3C90S+tV4e5B8JBMRXiK6Q56 - CAz00Iv0P/Qv9qWgE4PEZvG0Z5znf+GDUsySz6SWWvA8iGcL5tTH1GwMm8LK2Fr2MNvB - 3hBShDlCpXCXcLdwWZwuzhdXi29IK6VO1SbVI2p/97eePs9LnjcpnGLoNsRMK2Z3is7T - N/QDEzFWNDOxPFbAFuJpY3uEHraf9QhlrJ+dFw6x99nH7Es2IKiEAGGUkCo0C9uFDuGU - 8Kq4VNwh/lF8X/xWmqQSVPtVn6hNmn+6q90b3K968jwfer7HitOSEZ4poOm0iKow2yZE - 632YxWE8R+C1F+k0nVWej1k0XaHvwQKxEBbJMtmteKazGayOLWV7WS+ekwqW/xPgCMFP - CBbChWihXKgWGoU24U2hTYwSU8Sp4jzxCJ4z4lvigDggqaRQaZRUJJXQJqlR2o3ngPS0 - 1CmdU41XTVJNV1Wo2lQbVJvEGtVrqrfUreot6k71l+qvNEmaaZo7NJvgnbOI2Rd8a8Cb - SCwe6DNpOdUwK6umnfDGflZF7Yiuxewh8NVESZ4FYqtYJIxBNJykexGtu2ktbRDn037P - O+IhehuRsgzDtdGfpAKKUe2Cd+6nMYgi32NJTklOSkwwxctxRoM+NiY6KjJCFx42amRo - SHDQ8AD/YX5ajVoliQIjs00udBicCQ6nlCAXF6fxvFyFgqrrChxOA4oKb2zjNPB+Vai6 - oaUFLet+0tLibWkZasmCDBNpYprZYJMNzr9bZYOLzZtZCX2zVbYbnFcU/VZF/4OiD4du - NKKDwaartxqczGGwOQtb6tttDmuamfVYQMewNDPfOCzkzwd20pSqtfU6JLyFzRkpW23O - CBk66kSTrWqxs2xmpc0aZTTaUYaiWZWwkWZe6gRO2hiwWF680WWhagfXquZXOsUqu1Nw - 8LGCU53hstUZfs8nuh+z1zTbpusqnYKpsKq2vdBpcWwEuTzr4LmqTciVlhswrLDeXulk - 630gOMYGIOVwa2Ubx+VoMDj95AK5vr3BAXJpVmVnpCXSJldZ7U4qq+yMsEQomTRzj641 - z4jZ96RNTpvM0zyjrtWbfvqAt/z1fp7qWl/8AGnprCECGLcklwCn01CjGJEBNpf/1OZS - e00ueMKfnWGaS4FnilNAzIgmp8pUUuVsK78Go97qBedosHb6RUTyOTgK7GjvaA+aAE+h - fZBsaP+W4EL5yhc3llT5StSmoG+JV3JHD8WKk1Vd01sUYjDrep1cz/3bovgUeVlnu64A - eU4Nx+wc6cwsLas0Og12FLgo1VzqIr+yyqOMbbG7mGe9i6wxPXiDiYsWotrMQ22pFfaR - STOjIMUIbbTZUIhZF/JYMbQb2ksWtxsKDfUIJsmkpKiobbeng8HySvBEs2HRYo8aUmvt - 9gkYJ52Pgy5o3m7HCA2+EZAqRemDaDTGXAqvJJRVzqx0tlmjnBarHV5A+PaXVTr7Ebl2 - O1plDCEF4rVLdT7MmcCckYL6LO8o5RgDQ9jb2/mY5ZWy0dnf3h7VztebN+9i9NMCi6/A - RbwJJm5zsbYy9EUiG6N4gWyUjYBl55yORUhfiygXjftlhrOHcKNnDtBmKwzn/koMj78Z - hifcFMN5Q0hvYHgiMOdxhm/57RiedAPD+b/MsGUIN0BOBlqLwnDBr8TwlJth2HpTDNuG - kN7AcCEw2zjDRb8dw8U3MFzyywxPHcINkKVAO1VheNqvxPCtN8Pw9JtieMYQ0hsYLgPm - GZzhmb8dw7NuYLj8lxmePYQbIOcA7WyF4YpfieG5N8Nw5U0xbB9CegPD84DZzhm+7bdj - eP51DOODtwBn0vM4e4k4qeW7qDzVRdp0vPwg2iAcVs9DeB66eNFFEoSgay5Sr3K2q0jt - xSgqqkgdk5EVbAxOhBRIW1xXP1Kd+GGKS7p1oAufXwzftaQOgx1/0nMr6M3Pg7w3Q3+e - qnB+4aMwo8Yo+oR9KqUnXt2+UEyNv/pmg7jGNHBKdaLbXXDIPQID8nF34Yg5A+OG0nhl - XC9cEZBVEH9ISLoXIQWHjO+FTZxMFW24T+MWQ5mRyaGTWA6TRc0IphFldo7F7BU6WKT7 - zAmXX0bEYMXpfcP8U/xdJ1UnBhKkCz9MEWvSzt81kCy9nZb93tir/wksKZjjbGWOYBFz - BGWwr4YoFAIHPxWLinV/nzYmwyj7saxQluXHZMYivxKedXf8y8O+uDJ4L6v9zv2N8LXw - yuCrQubg2MFAYT56CdTkuYjzRgkF4kyZ52MzkcYpLOpxHquA+cTrnMf1lPMQIBkHfTT0 - 0eljMkyZOdn5bAQLZGoNnjCWnYMnQY5DTs6Oz8oMD9OI6rCszOwckCLHJSbk8CQhhxN1 - eVHNU/GxpuVZTbU5C8KCF7Euiz7Yb+SKezaXpkQ9nc50T5yoqzM8oA40BehDYsxpCQui - A1VFl9bs2BVjeG/PKnPJga2jotUjhkenL5k+TxipNevS5pdPSyn/657i4kcGd0XHieL6 - AHWBbClueO6hHU+Ggt9Vng+ltdJ0iqRE36z9cdbmsaPDKZ/PWofZMYRoCNIRF+FZ2Tcd - 7yyy1JIcJ+SEUFZmmLTkiKqi9ZnlRXHyvG1Nj2UeKXVf7nu9J2Mim/OP504IL9U88HTj - Y/svbrjrzdMs6zJOjhOcnPu1ngs46RXhvB4/FMlaGqmgiMSplKOJUdDAepgmTGPkpsFr - FsIKJMN+qGJfzE4E02qN9DuTislXv4xdsmvzkony0ZGNeTX32WadeSc3h83/aEX/3SMi - Rh9e86osPjhz2dTHnzi9ILsob+vosugghIuaCazgdvfWVYX3d7UjNIDP7M6TzuLkp6e0 - IXzBWGscVywZlDQRfCkrLSw8J0sEKCN8mqUO9wJVXKxg1PBA8MHPThC7zQkxB86lztnn - Pnv45VHHBf2YB84tyjUXHVz77Gu3jGdFva33nbx9giHx9jWnmidHp66RJHnKg1czX2m5 - sOep4sSJ2yrem1X2HYthw9nofZ2Ldj934kjNupf6gXkdgK/EuuF7UKhv5QjKavHtCFlY - j1kamdUd/+g4yz1uPi6lDLylOvEKYmID+q5S+gb6evK1JiC6WRYYeqHbfaab70R8r4Ad - 9QX4LoE3UdZnCJQoiAnW1Ir3sF4RO8OxQoxIw5GGK2NpsDL4ggifxLzrQg7loaXGVhHq - 8yRALuk0zMyru7NtcvyoGV2176TpYnf17Q2bd2vDcXnd8YfDAyOa6s6a7+6W0h+ZEX9L - fnxhRfmjs7cM5gif3V625cDgVqGvMbN077nBM9yXCl5pP/BGULgP7zBg1SnMBHk9mKW5 - Dg8PKHjMu38t6TA4+uovjY6M23b8P0YFRbVazDMKc7PC7uLWF87aN/fxwZnCE9UTFw8P - Kxh359JBfgkIX6zwvCudxxoLQIx4rfI9k1vrpbBrce2bMA+NkByBjL41FSJeMESn9T71 - ckJ87RNdz3+Q4/6z+7v3Xhw3gVV8eu5jIXnnwoevdnZcYoEd7kH3syz1KvYei/sLxW6U - e470Ova0ERRHeCEq3jFipqMUm73AEw0MeAHBK0EXexHJ0RSAnRR+VtDwSOUBnI1AUVwV - Igp8tSUmJIqy+EFUiKG3r3GCMTI0rrf1H4NPHYm1ldTfe+xUztS3H9q9uigltblbiG2b - f7Rv8e41cw+8IfzXlpKkie7PgfPxnYvGxZYMvgd/PO/5UvhCNQ/MXNvfg4GQ+fYejkx5 - RSIdhbgRkYaf53EoInZFLzrvRpqQEyrnZLGXj1k69B07AuJCM4bHjoo12hJb88N2bdVv - Vc1zv7l90JYb6s+ELX7a3y0RTm+HHbwzpC4pHem462L92jvGD1gERLH37SYNaVqfxpGE - snC8ZkT+pmGfH2AlF9zJTHX5OffBS+yKlO5+kK1WDQ4M/pNtcy8XTEoM8qgg/+qLf1oU - OPFbCvZe5f71fPD7vFxJ/d15yhtYwC7DeCn+kKqT3cm4Tmbft18dF7BNS4x/B/z4F6QK - oQJVBR1RH6JdSFOkldQkEa1CuhZiZi/ROsgG1K9DnssKSJQwnp5HO/7+HEsOOkAXWAV7 - hH0mFAgOoQX3h1rRKu4U35KC0ILjCcL9oEAN2FsE6EG0AF8Mnw0LgNd4LcMbxItaDb9S - 5cySwvK5qcW1y1pqm5fWVKXNqF629M5VtWgp4Db6G0gt7k3/3R+3l4SbtjyyUiHuYKfi - lnWGcgs8Cze7c3FHig8C/n1VAsmHjIOkpk7WURs7QH+APAYRaSnbSKshGyB/hEhD2kHk - etjGTklr6WWrKZJNtfhL+tkjI/S6Yf76111M3b1X/67u4z4WgVv2D1lE53DymzyMPcb2 - 0WLSs6fIxO4BsiS2uyt5md6BqoPUBGmDiMovYwc7YzP1J5mZTBJDnwSKldgx/acZafpP - MlwC69SfSnRJSF6IRc4SqO+P2at/PmaJ/iSkw1t1KBktjukPxizTb491sd2d+m0xLoY+ - W73Jqhh0PaZvTN6pX5yh1E/b6RI6OvXjUV9h8ddn5xr142Iu6dMTXVqGfFrMNH1Kxt/1 - 8eiIZgYMarIE66NjtusnoCo2xpY4AdLHDrE9lML2dJqm6nuhYrpdJcm5O13s3q7ipAyT - i91jyS5O2plcnGhKnqY3JRcmJkKvOKNZp7lNM1mTqUnFBW2CxqiJ0ozUhmiDtCO0Adph - Wq1W42LPdObr1X2sg/JBS0eXVq1VudizKJT62GGl8PBxraQVtKQd6fJ8gH/mMBrpYh3d - CAxGUI6pFU3tYoexFnjRYYseoYwNRKkIQoTxj2H+SwLTCgghJ9vsUtP6sJZ8XX7IpODx - hdaf+3EoNdd+U3/+T8dinDtxF+M8FGPHtRcUT4z9WnPdNeVn0+ZVqKotSE0tnbW6q6Wp - oU65xpNttQ7c5jk3tuBata3aYDja0OS7o0xwVNfU83ukqlpnk1xrdTbIVsPRFqUfL76u - uo5Xt8jWo1Rnm115tM5Sa+1ssbTY+HVmV3XBigU32NowZGtFwb+xVcAHW8FtVSv9fmJr - Aa+u5rYWcFsLuK1qS7Vii0/etrS8YGUzohNXfbhqSyp3lsycV4kbbbvVxQ7w+79V9P+8 - rYdICmVuZHN0cmVhbQplbmRvYmoKNTIgMCBvYmoKNDM3MQplbmRvYmoKNTMgMCBvYmoK - PDwgL1R5cGUgL0ZvbnREZXNjcmlwdG9yIC9Bc2NlbnQgNzcwIC9DYXBIZWlnaHQgNzI3 - IC9EZXNjZW50IC0yMzAgL0ZsYWdzIDk2Ci9Gb250QkJveCBbLTkzMyAtNDgxIDE1NzEg - MTEzOF0gL0ZvbnROYW1lIC9YUUlGU1crSGVsdmV0aWNhLU9ibGlxdWUgL0l0YWxpY0Fu - Z2xlCi0xMiAvU3RlbVYgMCAvTWF4V2lkdGggMTUwMCAvWEhlaWdodCA1MzEgL0ZvbnRG - aWxlMiA1MSAwIFIgPj4KZW5kb2JqCjU0IDAgb2JqClsgNjY3IDAgMCAwIDAgMCAwIDAg - ODMzIDAgMCAwIDAgMCAwIDAgMCAwIDAgNjY3IDAgMCAwIDAgMCAwIDAgMCA1NTYgMCA1 - MDAKMCA1NTYgMCA1NTYgMCAyMjIgMCAwIDIyMiA4MzMgNTU2IDU1NiA1NTYgMCAwIDAg - Mjc4IDAgMCAwIDUwMCBdCmVuZG9iagoxOSAwIG9iago8PCAvVHlwZSAvRm9udCAvU3Vi - dHlwZSAvVHJ1ZVR5cGUgL0Jhc2VGb250IC9YUUlGU1crSGVsdmV0aWNhLU9ibGlxdWUg - L0ZvbnREZXNjcmlwdG9yCjUzIDAgUiAvV2lkdGhzIDU0IDAgUiAvRmlyc3RDaGFyIDY5 - IC9MYXN0Q2hhciAxMjAgL0VuY29kaW5nIC9NYWNSb21hbkVuY29kaW5nCj4+CmVuZG9i - ago1NSAwIG9iago8PCAvTGVuZ3RoIDU2IDAgUiAvTGVuZ3RoMSA5OTcyIC9GaWx0ZXIg - L0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ab16e3iU1bX32vu9ziWTmcncM5PJZDIzmdxD - SMiQQMaQhHsMBCFBggkQCAg1YIyiwkGFIhGpQrkIPVS05RKKGUIKAxQ/ykHR1lPxitdW - j2i1T/N4Tg/aVpiZb+13Qgo+/fr5R5/OO/u+373X+q2199qXFwgAaGEdcBBeuKK9C54n - 4zHnFXRrF/Z0Zz7+xfi9AGQaALd8cdeSFYaP/uNXAHwUQK1dsnz14rJtr8wH0J0HMF7u - 7Ghf9Kf/XX4SwHMQ3y/vxAx1llSE6Y8wnd25ovu+RRUqC0AWj+l5y+9a2F4WHVuO6TZM - F69ov69LfkD9V0w/genM77Wv6Jj7wMPbMB3BdFbXXXd3000clmW9ienGrlUdXb945Hsl - AN5spO9VzCP4sJ8WRFiFYR2Mxhyq5F33uOuR4ZAH4Vs5yaSIgQQyqDBUgwbbZL8U0EEq - 6MGAcSOkgQnMSj5yJZwFvXAGcoR14OCLwA2QeBfdeyyM35b4TLgA+viKxP9wlfgGogQn - aby6Cs7C47AH+pHigxjPgfmwC14my+AkmQeD8DbJgEKUDw9RmAavkETiNVgMP8H63XAO - tsNRpCsHViAV02AL8SXux3QY4wtgfeIZyIYK+D6cgRC2ugWGEocSx7B0JtwGfXAY3/81 - 8dKjfFriucRl5HQGtrkeS15LTEv0I3f5UAONmLsetcLHvZfoBBtUInU/gh/DPvgl/JE8 - TAYTnYmexMXEx4iyDZzQhM8aMkg+5vr57yd+lPhDIo5I5EAu9toG2+BZbL8fn7Moqjpy - J+km28h2GqYP00F+g2CNxxCHIEzEZxLcBY8iAifhPPwJ/kq+pDZOz3VzLyTKEv+L8piK - XDJOOqAHn434bEGeThORFJMJpJGsIT8k28kbNJfeRpvpvfQ++hnXwM3jVnNv8HfzA8Jm - YZeoiX+VOJ24kHgLrOCC21Fn1iJ35+AiXIFvCIdtOYmPVJIaMh+fdWQPPUn2kZO0kZwl - F2kf+R35hHxJrlKBaqmZ5tFuuo0epufob7il3HbuKe533Ff8eIEK+4RPRZ/0fnxBfFP8 - N4nKxMeJv+CIk8GDkqmBBrgD2pHbLtTWf0MujuDTj1I7Dy/Ay8rzCXHCEPwFUQBiJA4y - ikzHp4HcShaTpWQvOYXP8wotX1MUBFVRA7VSJ22iC+gKuo6+Rddx6VwuN4Wby/Xj8xL3 - NneVu8oLfBpv5ifyk2Ezv4Lfjc9+/iA/wL8qhITxQoMwW1gnbBI2cwuF14S3xbXiFnFA - /FL8bylHmibdJW1G6byMOvtLZQRc93iSjdSPgu/BQlJLFsAOlMY+0g69qF2LyKOIVxfk - JFq5tdxEWoza8Dw8gNq6G9bAJm4e7Eu8w/XBJdSU5djgOjjA14BL2InSeRiKUYuGn3Aw - N5gT8PuyvVmeTHeGy5nusNusFrMpzWjQp2g1apUsiQLPUQL5dd76tsyIvy3C+72TJhWw - tLcdM9pvyGiLZGJW/c11IpnsvXYsuqlmGGsu/lbNcLJmeKQm0WdWQVVBfmadNzPyn7Xe - zCiZO6MZ44/XelsyI0NKfLoSf0KJp2Dc48EXMutsnbWZEdKWWRep7+nsrWurLcgnJ8MI - h7ogn00cYdCwhiMwoX1Npw0DVqMu4vDW1kXsXoxjGeera18UaZzRXFeb7vG0YB5mzWzG - Pgryl0aQTnhMu8i76LFoGBa0sVj7vOYI194SoW2sLUNexOqtjVjv/9T2t+T1WN3mGwoj - 1Fff3tFbHwm3PYbgsmQbS7VvxtTUpkxslm5oaY6QDcNEMBqXIaWM3A5vHaOrbVlmROWt - 8Xb2LmtDcGFm84Aj7Kjztte2RKCxecAetiuJgvyTtrWVHuT+ZMEtBbewsNJjW5sMf/9I - Mv/1syy0rT3/EYZTZ44AQFhP3slIZyRzodKJF4mtYF5HBfQurECc8NdCkM2lSM+ECEWd - 4XwRwTe5PbKu6ToZnbVJ4tqW1Q6o7A7GQ1tNC9Zv69WPRUlhfb03s/crQBF6h/54c077 - cI7o038FrJAJekRXIqT9erxHAQa57rR5O5l8exSZYtprq7shA9MMGkZzxBQZNbWx2RPJ - bMGMKOTlT42CqrH5KCFbWqIksSEKta6TaM+4O+ZjcT5TtaW12D8mCvIxI9eDscL8zHrk - up7pSmZvZu/kRb2Z9ZmdqEy8TwmxoKO3pQgRbGpGnGAW9hhuSR+JdrS0jMV2ilg7+ApW - 723BFpYNt4ChklUUw0rF+VNRKv7G5hnNkXW16ZFwbQtKAdX3bGNz5CxqbksL1ioZoRQp - XrPUNkzzKKS5JBfLS5OtNGEb2ERLby9rs6nZ64mc7e1N72XjLZmOEvh2Rng4IwqsCjJe - FyXrGvFdDLyedJbh9Xg9SFYLw3Q0qvR1jYpC2T9GuHyEbnxzDFJbriBc8U9COPRdEB77 - nRCuHKH0JoSrkOZKhvC4fx3C429CuPofIxweoRuJvAWpDSsI1/yTEJ7wXRCu/U4I141Q - ehPC9UhzHUN44r8O4Uk3ITz5HyM8ZYRuJHIqUjtFQXjaPwnh6d8F4YbvhPCtI5TehHAj - 0nwrQ3jGvw7hmTch3PSPEZ41QjcSeRtSO0tBePY/CeE53wXh5u+EcMsIpTchPBdpbmEI - 3/6vQ3jeDQjjgrcG96QXce/F4Y6tOgpNeVGQi9D4oZP1uFm9iI6lMc59EAUeHWBc+gBO - 4RsAs/NOYSsChsUlpQaPIYCuht8SvfZfwplvJkT56VePYS2K61rgh7AfthtsCGdLGTyv - 4TJwh6mSM9QaWUu1WgriUlqpcug42Qf2FF2UaI55tm+y5eU1XJkeq2rQfz39ymWDMVQE - 1dVVsarqqiGMx0qK0zxmj2HYkX6+6No2Lu/aW9yDV89Rt3BmMF7TF9f1Y9f4IwodfZhQ - QShsY1SohqkQ7yQOjdKzWhMlc7DnD27s+TLr9Nsdevu5q9deoa/Fii4oHfXHFrE+dgKI - VuwjDX4dbqklUzkqEhVnIXbuEhHSiJMzadK1c0gz9yZ5n3tT875Wzav5lDr6fcrPoDsp - DapzUirUFSkT6RzaQyXfohQ15YwcoRqtkRNls9Xq4HkhSvaEU9RuTiPGtITGUtxGzDme - BnZTT5ctr0F/pWp67LL9SiiEf9tlhl9dR+1nUG1F5IzW0NSZq4+maKOkb5ASyljuG6CU - 2yhML7w/xq85v1FIhiXF0LpqJVnVujLNoyIeg9cwuryMeInZZDEbvDuJi+wnzxLHGT7e - +kJ8rvC8cOaqn3/vmwncwoKL914N8pcKyj8cfe3fFR3oS7wrFCEuZrBAVdhrFQJChZ5T - AxXG6lUWzmIxqXxah434THar7WnPdmSDiX6ISZ5BjyIYqq5qLSkmBpPVUjpqTHmZodSg - l6gnk/PbiYd0V7W8Ebu95FeTvx/fHN+8YTKdIJy51v30sqePzP8xt/nahfj/bI1/TdRb - SSoXQjmNxpOHcqRHhB+Ea58gTxMaJrMItRByn/AZoUv4TuFRnrPnUJ+R43jwGUVRIALl - RA5J5mWZyYFyewUge0W7tGW+Lc+OsNumx0Ih/NsbGN42qK5CyI0hsnF6Yd7GQlseAh9G - gRHgeNyTUlHYKK/Rn1c85KwVWleuXKWipYgx0SO4+34X+/yN2BeIq4v/5BtkiOkxBzMT - Hyi7z1Q8V6iCD8MVucVErUe9cgZKJ+mXqpbppZBs1Kq49FFStsql17oq82hhsPJEJa0c - lesz6iVBdgayrM4o6UVRuNxSwFWooa4yTZVUVeU0ScHcg9mO8elB55TUQIV93PhfkJ24 - 6T5JdsCwVK4ocrkcO39dMkPVQyglA+pWK47SwqHCIYKhwRoqKZ6wOpxTPsacBcTuI+Wp - HrBlpHvAkmnyEE8WjKEecLisHmL2oAd5eXlEX4V+3kMPPQStpDVbkfU4oiOpRJREMylH - yY/2e7MkUfKOJ6WjcPtqMGEl7EJHvFkBf4AF/rLR5WPSiG5Vwx0tOzydo1YsKGkig+PN - 2kfuf7zSoz4o/PnZMz33WH3aDENuvr8116Ia85sHt585tbP31bn5k/c/aXaKuhRn0RKy - XM63Fcxrmpbb9OKeSZN2xXY6szhug1as8YYnLfv5o9t/kkYuszkOTye4i3wDOCAdDoSL - DtjJLttBuc/GTZENe0wcZxJdDinFhaNfSk+36gNGwgWoweFSB6x2pytKpGOeVWv+pvNV - 04dCoRG9ZxH9kALlaLDLPq1Z7Qddmt5PjIZUvWTHlACchxDKcxpLih9SjeipbKKf8ET0 - EIYnwsqATfp5CrZgsXoLESyENYlgKYOOlumhVKJvf2Lt169a+7MpxY9u7XrE3p/x36df - /4YY33TyDZFLCx85uOLpfR9suvetF0jpZ3i0MlZADCoS73FDwjmc511wb3jUGN1E3Rzd - Af5QuuCTTTTVpQfZ5ZLS1NRl1QiFaYX6oMHocGsCDnuGe6NnVc2N7Mcu46w7xAa9IWRI - apHD5lSpgRCbBnlzogd26gd1uuxHBvGvaIyRqYKiIKIZrBYrThLeMsYWlI02ln69dd+a - ffvvf/QQ6W0qHnfkmeqf3XUs/s2XvyV3fH7p5V//x8Vf0TGjM6ZS1zfjty9sJgXf/IHM - wfE2KfEe78DTHieeDPqINrx6p/yU44CbE3Q0VTCZdcZUsymsDZvkoINM1RznLpAXuQvp - 78jvqt52v+P93Pq5V3PBcMFI58mCJzt1t8WVHRIlyeJxOSW1y6LxSTudB5wnnJecvM+S - 6nMKdrVWMugCqa6A4AhkF0oBu90feNOzvzUJUOyyMim+GQsZQzjkQhgUtSrzI9MTtI76 - IcxVtKUevLzA4VEaEXjR7Tfojfo0vUnPi1pfVnq2HzLB5ScZLpVV8oPGrPOTFJ3X4cEs - AT3ZhnqVokePDcvkuFTGZm5e7kNkZSusbG1FFcLH7MnAkTimfAwqEI5LEdE2oBIRfwAH - qigROvh2RblRf+1L4Ymdj88qNh2Vbi2ZufqWmS/F/0Bs/0XcmpwpRx48KBAvP/HO22Ys - n/LMsy+0lk+sfLKw0anHuVDEGbMm7r+n/uFjveSD5Bw4Ll7JfY4ycUMBnvSeCE8vN02W - J6ua5RbVo9pD6QddhwL7806ma8IyZ8kK6s6rs3Ca48Wgy642utSphVJhoeDkCi2FBUHB - UazVBVLG+wNOe1HxDYp4ZSjEkI5d/grxTFogppEKvEl88705jgyNIdun93sz/H7IcaBn - 0Og8kKrTpvhcWX4SSA/ieNQaPQqKw6MQ4VS0lWloWanBJImeLH+gFKFkMCozWDZDEJSJ - ThmdOO0R+uD80rL9VV3xl4/8UXciJTDukVfDfq5815rn4leJdIrU/uTfnq/3bXvw3K35 - 8df4mvHeCRuvjXql5709P50UqNo6+8OZjX9Go51CCuP7zg7csfvnZ/oXrqcFCCjB02pQ - xq4FmsL5qJ2yVbLKAT6Qdo90jyynpdA0PLE3uETJrFWnBNVoqc1BsKCtjhLxmGdBcuzi - qkNZqg0xw8dGbogwRYTWtFIDztvJyRpXEYpa4BJi/WC4dM7DXzQVnMwo2dh1fFA4F/tg - hif0bMve2Az6bM+Y5t1vx15i8qaMPlKJBpCtVcvDTulTHokWObUKDTHqR1DicGJU9f2N - kvOxqvOKFWaLt+rpOHsiEV5Dqdm7/gT++Nyrbwtn2I0NgU3ojVPaDoaRS04tYKPYJnB2 - XrihSWQuuYyqTja2aXCQLXSv4yf6+Inghw3hSkmWdGKqVbbqrKkBOYBDeZJ9tmaJRuv1 - qR0ur11NeavP47K6UkQJxHSnj0tT52CfhqApSsiAI4gGgYRxriv0ofLYAzlRknIjyJf1 - V4auxIaJwTVdNZoLHPPWEDO61xE3DyNuvW4lEXg2HBH3GyQwEB7dsnJdQ3521TMd7zTk - nr5z+rKnTjiCXYsPDPJFu27NHledXT+76UeztsTG0M/vbNyyP/YkPb1i1NS9rzLJKHLh - hnAc2tHyzQ+XnBAviJQXTWLA1CN2S4JJS002vUtANm0atUNyOEAbVDmcpNAWtIM9HZcg - N6lPcmpLjjbka+hvKkRQicw3sMJ0COcaHUF+yPrD0/o6Lzfmn3AVrw0Hp1QUpA+SA0j/ - /Jk/nvMM06UFVYtSLDVlK5fGXkViUYsqE+/yHrTXWrx/scMT4dJd8g79U5af8gfl/fpD - lqj8knyJ/1T3hUk7VhZdNknrMmrskt1upoFUR7oqYLY70qNEhVZ7eFa+eaWaNNb5YOX9 - mjQVzqAG6ieSFWNCCsbUJq0fiB492YJGmtOhp8yxzGPGOdtYNjxK0DIbcTalHrRgimH+ - aEPxtFM/3bHjWbzkuhb/84fxa8T4e7GbpO7fMf+H1wYOX+bei/8xfiUeiz9H8q7hwinM - bHNP/Dbeh6zrIAu6w/mH5ANWmiNnOg060WWWUkWdy6nJ0tGAzZGtLtQXeoJZqXZv9kbP - mSR7bD+RlI1iaJhghk2M05IOgsPP+yEdGRMs6BG7zg+cVeFJYYst5bLRKidlZmYLeFKa - 1E+8eGD2ApdtBi998YCv/tTpOh/68cL+8vDtDxyPn+jevXpmceXg6jdeXzfv6OlFux+c - s587umVyTlX8C+TxmR13lGVMjn04PI7pVhyDBrg17A9w/pQx3ESe18l6qlMZVNqAzNTQ - oJYdaYStPcBuTIuSOhxYaxXDynhs0OMuqXp69fnYeWZZ2XhKzl+K6lmsZrZeYkNo02Hz - T+4UbC59uv7RrThUTpbvodzzHO1fFdvFxkVN4hJ3nJ+KtqmIFIZ/UKHaJewwPmXaZd6V - K+Zk+wLlnnrPxOyJgdnZcwKLs5f4V2tXp6zW9Xi7s7t93f79GQfz0zg0yUIBX5gGDnO6 - 1WkzF5gKc1I1S2W/r9xHfVkpaj4vzfai05Um8a7C3XmaIkml01MJijxFDrfNYgtYx+f4 - pUCOo0TnDujHQ6DQXlwyMLKOwCkkad9CeowxdkNF6OOQYzJmK3o2paxUFhLTSAH1m30O - v0fn9oDKL3kIl497AiEXYy4j5qWbbB6SmZrlAU+WLkUOqD3E71OpSQHvATGIXobB6SF2 - C3rKckJZiCqeoiLXFR+X/GmKGVTUpYgtIXApzyyH5E0uJ5j6uAlbdZhQcfwB8qXsqz24 - aNe4wN0/2HRL9/sn/3TnBNon+Mc/tXhpXU7Dvedqlr772y8vSOQEaZxbPGfO7XXZuALL - yp380K5fbJnbOW7UxIZwfa49zVWUX/fDH1x892n6V7QJ1sSXVCXMxdlh5s9TCtVndSRK - qsM+3hKycqJObXDgdI03nUEw68ypnJuj3DWL3e645lkyvIqPtYbOK4ux5DRdxCbpWNWQ - PnZZMR5oh5Ib2eF9i78M16mlB48fPuw3l6RkmNwTAmvnPvmkMDf+1rZYXUWahtAtKvmh - JfQFvNlH/VqX+IT7LY5nK1I4Pzw2anrJRFVpssmeZjfliPdyl9CEg6BTg5iiFnDuskk2 - G24NCtVBrcbhIEFG7OvXraWyzWbqj+JPrnOqq5hCMNUnrTftuL1jlPUdSsXgIxWO4kd+ - Uesb7KPe0Uu2fdpUwI5gYqGZo9sOzv13qrv62t5xubOemrmJvuNg41ODE+/HfBGGZey0 - Ce/m2fESh05kx0xF7DRJxKnSGDqFN/fXY/JwrLgkrTSdWFXEi3+S8cXXf30/vpOs/iz+ - dTx+mazmi+IbyWohdjX2Ptka/x71IUzYp/L7UX116x2pVV+BQVbSL140/I5FlFATrxR9 - uGsBPBcars9CMRgP4icR5C8d14Y0T46UKO+jZxWMUCPMhn7+E+gX+2AnfqfQh+nR/N0w - kweoxLAC3SR049CtJxcUtwnrrmdpdKxOD+2DTVi/hobQWNwN6zCOOOH5xH3QR8rJWvIB - 3c/lcD/k5wsi3iyvF/aLWXin/LXUKT0nb5F/q6pQrVKos+JdOAd34vqI4pcWemjFDzE+ - V2sRScYVwS8TktyJWAbNLbfNamzKm9SxvKeje+nCdqxB0eEv0YHfBvy9nxUzc/Arg2L8 - OiIEtVCvfG0wRfmi4Fbli4eZ+BXDbTAb5kAz3J48T5yMZ4rV6MrQ5eXdYoN1ZD88ge5p - dBwsJY/BanSb0D2Fjh+JHcLUSfLYAC+HT5HV4CBTwhrePctkd9vUGvfruGwY3Ot+1/bJ - aWLHr0s+JvaBFFDdosaDnB/DInCTn+JO7X78GiKH7D4WXO5uw6JD0IVuHTpO8Qk5NJAx - yv08yQcfHse4iR8yeHLc/fuSAvenJVFKBtznAlEeg19mYCqc6j7r2uv+P64l7ufRHU4W - 9QWxxnH3Iddy97aMKNk94N7KFm8D7ieTwT0ufPW4e0Vwh3tRiVI+bUeUHh5wh7B8dljj - Lq/wuMtcl91FgahMMF3gmubOLflPdza+iNUysVFf2OB2ura5x2JRhqsuMBbdadJH9kAu - 2TPgm+I+hVFk99jkYMWOKHng2KScEl+U3B8un5SzIzgp4AtOc/uC9YEAxme/JK2Xbpdu - kUZJefhBAk7kUrpkko2yXtbJWlkty7IUJT8bqHaLp8lhqEZYDh+TRRmPHJ/DTP40OaJk - Hjkh8zKVQTZFEx8NMv3CpevhQVQtAhg5LioxMUqO4BkwyzoSdqNqE+CVAj1qW/ITI1RK - SmQKU/Dm9/GoCBssPdW2auN4Q6i+9v/ltSkl133FdPx9z0ZckR149xjpc7XgNS9GEq6W - 61XR6P9/ft33YIWOmjx2bnesp2vZYuXa2lvX0Ya315HHevAzgnULMjOPLusavpP3ty1Y - 2MnuTds7Il3ejtrIMm9t5tEe5T2WfUPxYlbc4609CovrZjUfXRzuqB3oCffUsev7Ywtq - VrXe1Nemkb5W1fydvmpYY6tYXwuU977VVysrXsD6amV9tbK+FoQXKH0xCOqWNtXc3Y3a - iVfbeLWc0xSZPGNuM37B0VIbJfvZffc98H8BcaX7nAplbmRzdHJlYW0KZW5kb2JqCjU2 - IDAgb2JqCjY2ODkKZW5kb2JqCjU3IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3JpcHRv - ciAvQXNjZW50IDc3MCAvQ2FwSGVpZ2h0IDcyNyAvRGVzY2VudCAtMjMwIC9GbGFncyAz - MgovRm9udEJCb3ggWy05NTEgLTQ4MSAxNDQ1IDExMjJdIC9Gb250TmFtZSAvWFlVVFBT - K0hlbHZldGljYSAvSXRhbGljQW5nbGUgMAovU3RlbVYgOTggL01heFdpZHRoIDE1MDAg - L1N0ZW1IIDg1IC9YSGVpZ2h0IDUzMSAvRm9udEZpbGUyIDU1IDAgUiA+PgplbmRvYmoK - NTggMCBvYmoKWyA2NjcgNjExIDAgMCAwIDAgMCAwIDgzMyAwIDAgMCAwIDAgMCAwIDcy - MiA2NjcgMCAwIDAgMCAwIDAgMCAwIDAgMCA1NTYgMAo1MDAgNTU2IDU1NiAwIDU1NiA1 - NTYgMjIyIDAgMCAyMjIgODMzIDU1NiA1NTYgNTU2IDAgMzMzIDUwMCAyNzggNTU2IDAg - MCA1MDAKXQplbmRvYmoKMjAgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1Ry - dWVUeXBlIC9CYXNlRm9udCAvWFlVVFBTK0hlbHZldGljYSAvRm9udERlc2NyaXB0b3IK - NTcgMCBSIC9XaWR0aHMgNTggMCBSIC9GaXJzdENoYXIgNjkgL0xhc3RDaGFyIDEyMCAv - RW5jb2RpbmcgL01hY1JvbWFuRW5jb2RpbmcKPj4KZW5kb2JqCjEgMCBvYmoKPDwgL1Rp - dGxlIChVbnRpdGxlZCkgL0F1dGhvciAoVGhvbWFzIFJpc2JlcmcpIC9DcmVhdG9yIChP - bW5pR3JhZmZsZSkgL1Byb2R1Y2VyCihNYWMgT1MgWCAxMC41LjggUXVhcnR6IFBERkNv - bnRleHQpIC9DcmVhdGlvbkRhdGUgKEQ6MjAwOTA5MTExNDQwMzFaMDAnMDAnKQovTW9k - RGF0ZSAoRDoyMDA5MDkxMTE0NDAzMVowMCcwMCcpID4+CmVuZG9iagp4cmVmCjAgNTkK - MDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDQ5MjUyIDAwMDAwIG4gCjAwMDAwMDEwNDEg - MDAwMDAgbiAKMDAwMDAzNjY3MiAwMDAwMCBuIAowMDAwMDAwMDIyIDAwMDAwIG4gCjAw - MDAwMDEwMjIgMDAwMDAgbiAKMDAwMDAwMTE0NSAwMDAwMCBuIAowMDAwMDIxNzk2IDAw - MDAwIG4gCjAwMDAwMDUyMDQgMDAwMDAgbiAKMDAwMDAwNjEzMiAwMDAwMCBuIAowMDAw - MDAxMzY3IDAwMDAwIG4gCjAwMDAwMDIyODQgMDAwMDAgbiAKMDAwMDAwMjMwNCAwMDAw - MCBuIAowMDAwMDAzMTYxIDAwMDAwIG4gCjAwMDAwMDMxODEgMDAwMDAgbiAKMDAwMDAw - NDIyMCAwMDAwMCBuIAowMDAwMDA0MjQwIDAwMDAwIG4gCjAwMDAwMDUxODQgMDAwMDAg - biAKMDAwMDAzMTA0NSAwMDAwMCBuIAowMDAwMDQxNjkwIDAwMDAwIG4gCjAwMDAwNDkw - NzcgMDAwMDAgbiAKMDAwMDAzMDE4MCAwMDAwMCBuIAowMDAwMDA2MTUxIDAwMDAwIG4g - CjAwMDAwMDkwNDQgMDAwMDAgbiAKMDAwMDAyNzM4NSAwMDAwMCBuIAowMDAwMDE1MjI0 - IDAwMDAwIG4gCjAwMDAwMTc5NTYgMDAwMDAgbiAKMDAwMDAyNDU5MCAwMDAwMCBuIAow - MDAwMDA5MDY1IDAwMDAwIG4gCjAwMDAwMTIyNDYgMDAwMDAgbiAKMDAwMDAzNjYzNSAw - MDAwMCBuIAowMDAwMDEyMjY3IDAwMDAwIG4gCjAwMDAwMTUyMDMgMDAwMDAgbiAKMDAw - MDAzMzg0MCAwMDAwMCBuIAowMDAwMDE3OTc3IDAwMDAwIG4gCjAwMDAwMjA4NjAgMDAw - MDAgbiAKMDAwMDAyMDg4MSAwMDAwMCBuIAowMDAwMDIxNzc2IDAwMDAwIG4gCjAwMDAw - MjE4MzIgMDAwMDAgbiAKMDAwMDAyNDU2OSAwMDAwMCBuIAowMDAwMDI0NjI3IDAwMDAw - IG4gCjAwMDAwMjczNjQgMDAwMDAgbiAKMDAwMDAyNzQyMiAwMDAwMCBuIAowMDAwMDMw - MTU5IDAwMDAwIG4gCjAwMDAwMzAyMTcgMDAwMDAgbiAKMDAwMDAzMTAyNSAwMDAwMCBu - IAowMDAwMDMxMDgyIDAwMDAwIG4gCjAwMDAwMzM4MTkgMDAwMDAgbiAKMDAwMDAzMzg3 - NyAwMDAwMCBuIAowMDAwMDM2NjE0IDAwMDAwIG4gCjAwMDAwMzY3NTUgMDAwMDAgbiAK - MDAwMDAzNjgxOSAwMDAwMCBuIAowMDAwMDQxMjgwIDAwMDAwIG4gCjAwMDAwNDEzMDEg - MDAwMDAgbiAKMDAwMDA0MTUzNiAwMDAwMCBuIAowMDAwMDQxODczIDAwMDAwIG4gCjAw - MDAwNDg2NTIgMDAwMDAgbiAKMDAwMDA0ODY3MyAwMDAwMCBuIAowMDAwMDQ4OTA5IDAw - MDAwIG4gCnRyYWlsZXIKPDwgL1NpemUgNTkgL1Jvb3QgNTAgMCBSIC9JbmZvIDEgMCBS - IC9JRCBbIDxmOTA3NjFiZGExNmY3ZTJlMDkyMjA2Mjg3ZmQ4ZjAzYT4KPGY5MDc2MWJk - YTE2ZjdlMmUwOTIyMDYyODdmZDhmMDNhPiBdID4+CnN0YXJ0eHJlZgo0OTQ2MAolJUVP - RgoxIDAgb2JqCjw8L0F1dGhvciAoVGhvbWFzIFJpc2JlcmcpL0NyZWF0aW9uRGF0ZSAo - RDoyMDA5MDkxMTE0MTUwMFopL0NyZWF0b3IgKE9tbmlHcmFmZmxlIDUuMS4xKS9Nb2RE - YXRlIChEOjIwMDkwOTExMTQzODAwWikvUHJvZHVjZXIgKE1hYyBPUyBYIDEwLjUuOCBR - dWFydHogUERGQ29udGV4dCkvVGl0bGUgKFVudGl0bGVkKT4+CmVuZG9iagp4cmVmCjEg - MQowMDAwMDUwNzk4IDAwMDAwIG4gCnRyYWlsZXIKPDwvSUQgWzxmOTA3NjFiZGExNmY3 - ZTJlMDkyMjA2Mjg3ZmQ4ZjAzYT4gPGY5MDc2MWJkYTE2ZjdlMmUwOTIyMDYyODdmZDhm - MDNhPl0gL0luZm8gMSAwIFIgL1ByZXYgNDk0NjAgL1Jvb3QgNTAgMCBSIC9TaXplIDU5 - Pj4Kc3RhcnR4cmVmCjUwOTkzCiUlRU9GCg== - - QuickLookThumbnail - - TU0AKgAACkCAP+BACCQWDQeEQmFQuGQ2HQ+IRGJROKRWLReMRmNRSBP+Nx+QSGJtaSR2 - RSeUSAAysVS2Uy8ASaYTOaRNqzcVzmGvx5vN+AkAPd+A0IgiDPx4PB7hAIAl3ut7hEKg - 2Gvh6PgCAwEASEO9zOJ5gQJB4LAyKPx8VgEUaLTdqzkVzWRTK5XW7Qa3XCHNhWIpkBck - CN3tpxvoGDASghhKlhiAkiltOkPCwAOgEhYAOV7BoQABzOwGBkOgp9NFyAAXhZ5t15P1 - vtd7ksskB2M5pP4IgJ8AoLgVstUCCMQPF0v0GvJsPAHiABvl+iMUgpltcAFInDi2Qu8z - q7xq6d3wTPt3GFvNwr5fst9A4PhEEgMAPp4PF5v0DAh7uBtu4RC0MGQWhpAmFQRA2C4H - HqdZ5AIAgBgSCoKAcALiHYeR8AEAB+gIDoeBqCxXk2W4KhIDgUBiDJlFmaYLAsBJ/AwE - QRAafZznaegBgMCIFAUAADACAoTh6HAIoa8bwow78jyUkMjJq8xwnwD4Jn2bh5AcEgNL - Mgp+HUYZbGcD4jCQzCsAIrauIUfBummcoNhaEbsgAtC1TiiMmyWickzxPaLpIa09Jef1 - BHpQgHUMiJ80SftFx3HjwpWAKWhVPiJUBSlL0wgiOlFTheU8TVQATUSHGdUpC1OSFUg7 - VdM1ag9LVdWK7rSfByVsA1cAbXQI14iNBH8dtgndYddAaDNj0hWU+VhZVmpQdloWCdtj - gzQwHJBX50W0etuA5b1RKBZzwWZcVyoofd0VsclIW8DkGTQmCenmc16AlewK3xcy5XJf - V+oUeOAW0dAKYICeDSOfmEnLhdfg3h1cANfy5oHiWKoRX96HNWlVg6A+PUzYZ3HXkcWA - tXkiYtJGKZTlJw5djwDgvmVk2UfWbXVmQLgXneWI5ldyz9fmeotSFJIXoOf6GmmipboV - MTugp3mcWRqAmF5xkyXgajEH4KA4Cd5nofR/HuaBamYGwzisBByHAfMMHgdZzHqCYTAg - e54AoC4CHYfgEGoVZaAYHogBuDgJR6fp0nQf4LgUex7gUAx2nOfQIAAeTlAqB54nqBAS - g4dxtnIDgWhRLSEagg7xnUZxkHkCgFnCcgCA+Ch2msdoJBoDoBGwb4CBiD4APqeh1Ho5 - YLn7vqzHQdaxgSZBgG4FQdheAB6gGBZ9mmZB4BMJIYgqfB3HUe4CgAfZ/gUcJhGAAoWB - uEQHAOAoFcVxnHchyXKctzDmnOOedA6J0jpktHjacpd1TxBwjJFaLgZI4xzD9ASPIZwB - QkhoByBEfAwBbjbAmZgAYDgMj4GWKwd4Ow0BMAkOcaY+ANAiAkAIfI+B+gCK2OMYQ1wU - BSCEOoTwhRvAjB6iYFI4xWCaGeAgE4NgbA1BGUUfo+R1DsHgAMBgEkdAEH+BQE4NASAR - XgQWBcZCcE6HwNgSIihnA4B4PoYI2gAgZA8CgzA8gFASAsPUYQsR1gzCGCEBgDwCgCHo - OkZoyBpj5ACCEFINAMAOAmCUBI4BPixHcEcH4BhpDnAqC0DYDQAj7H0AAegyRcjfA2Dg - E5YwXAhFyIEUYJQkAkAQB8E8SIlRMidFCKUVIrRYi0AqLkXowRigQ0lZ0ZS0DvHeT5OQ - 8x2jxAQBhw4CB9jgHAO8B4FwInwH6PgdI7AANeAkAgfA9m/AOK2OcZorhcjtBcFIFQBB - 5AGAWBMBABwAG6HSPABDiB3DtHQNIZ4+gXBOBsBcfAAAAjwG+OMBwIQSgOTQmYrZCYyk - ESalwdRuwADuHIPQfq9wDD3W0PsCgEh9DojwBByQDAHD+W0PgCE7B/AFAMAoBoECzD0H - MNwcg/AKAYAUP6dM/wCAhBABaew5qhgSAcPYchuwIFbAFTqcQ7KA0hoJQahFCqGUOohR - KilFiCUYK5Mkjy+qNtKIxW+t9cCRVsX60itpMGRjrFvX0Klf1GkXGvYNTgog22HZyAOx - RF2mKTIVXiuhNbGwJsiQ5dA+xdWZHPZsMdnWaEVI6W5k68lir2AkuCytdLKWpIQrRlw4 - QMWxAfbMmiv5nDvZCAK3VpimAQs/axctq7gD2uIOO4wGrkLFTwtwerIbmW9tMxC4Cyrh - WRHldezY52OWBVcwkflt2QswtMta6al7qtDtvXsD962YMVuuPJkKiR8sntMu+8p3bzsW - uYxkEF/QC3/sizYfTISkjwZ2Atal9r7kwI7ZC6eBVi2KPgSKxpBcHF2I6vJd93LgYVYm - P9IyahfDRASDUDA7R1ASTeVS1o6hzJWA0BYtg9BxDTHOAIBgDAIAdKKmZeA5hkC+GkPs - CoNwcgtAilwbg6x+gOAqBnGSaRzDDGMN0DIMQcxhTQWgfmPktj4YS20c6uAIYxAbl4hS - d07j4HeOoAADQHXwABVJBhRi0gAx9GMhA9HujoBSDgEiaM2DfGoN0eAEAPOgnYWvQQ08 - qD7yuDKMI/B6DjGgN8f4NAWgeTSM4XAwB6gbBUD3TeX8u0ZIInMrg9x1j0zeU3OoAMwZ - oLxGc8hIbQ62AAO8ZIcRBDJCCDUCgCAJgAG8PkEAFKIkEMsDEFg/cDAhAiN4Yo4HtATB - AB4Ao6hkjHGuB8GAJx7juH2A4dw3h8A6CwDgfYxBdbWAiBcEAHAKAQAuP8bYrhpgQBQP - EWw3wKg8kEA0GANgECoFEOcJAPgGDvHkOsYI0B7hPCQC4c44BsjRGsAEDwBh2D/BICoC - 4Ax6D1AcCoJwPQSRmLedwACdx3jYGmPveY6Rqjf5wOQBYDgDD1HkOiCYIQKDhFyO8EgO - wUgbAAM0a4AgHjaGWAAHwOx4ivF6CkNYSBzi9G0AMDuMj7gUAYchgYJgDDyA8DsE4tRA - ChBSHAJA5BfDyB0EMEg+x2wSHlzEdIBADjiHUCsKwSgPjgFeLYeEkQFGcACN0Yg4h+gK - AWA8GIJQKD7ANPxuI5h1q1G4OAFAUQyhOyRywvWH0jHmGSNAbo+gFgaAoPocA1R7AMBF - mXbYzh3AbBuCkC4Bx4jkGcNEc4/lvApAcPvhyGACDzHiMwZI2QPAxBsD4IoMoLC4GN8U - DgGASgKHuN0cw+gFAWA8pMawuh4AfBiAVygDAKgLKeP8DoEh7DlRuOwdA/QXAoAAGkHI - AcA4AWH4H6HoWC3KBQBqBOAgAGAKRYZQ5e10o5AmTkHUGmFgF4GySCB+9+H+W4AAAgAW - HkGyG6HWHUGyGyAWBSB+BeAyHIFoGUH2Bm9u72HYHOHGHkBABsBWHw6AH6AWA8BSBCAQ - GqGEFcGkAAB4CqB8BWMwG2FgFsGzB3B6HWWGHKHaiIBw3uG8GAGsnKH0HUAGA4BgeGG4 - 3QAGHkHYAOBSA4HgGuHYHyGgGQHeCiDCB+AEHgUSHgHAHhAeckAA52BMBaBIdQrmIs1y - 5a1uVmHEHEHcA0A8A0z0Lq1ULWIuzVAqUyK8HEzmA8KKPC0oHeH4AZE/EvEyI+waJKmU - wWJQw8AAwvFYTxFcJAvzFjFtFuJOICAADgEAAAMAAAABAGsAAAEBAAMAAAABACEAAAEC - AAMAAAADAAAK7gEDAAMAAAABAAUAAAEGAAMAAAABAAIAAAERAAQAAAABAAAACAESAAMA - AAABAAEAAAEVAAMAAAABAAMAAAEWAAMAAAABAZgAAAEXAAQAAAABAAAKOAEcAAMAAAAB - AAEAAAE9AAMAAAABAAIAAAFTAAMAAAADAAAK9IdzAAcAAA9kAAAK+gAAAAAACAAIAAgA - AQABAAEAAA9kQVBQTAQAAABtbnRyUkdCIFhZWiAH2QAIAAUACQAIABFhY3NwQVBQTAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLEdUTULFSMSs5/UDDo/MsBg6 - 75uEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5kZXNjAAABLAAAAGByWFla - AAACFAAAABRnWFlaAAACKAAAABRiWFlaAAACPAAAABRyVFJDAAACUAAAAgxnVFJDAAAE - XAAAAgxiVFJDAAAGaAAAAgx3dHB0AAAIdAAAABRjcHJ0AAAInAAAAIZia3B0AAAIiAAA - ABR2Y2d0AAAJJAAABhJjaGFkAAAPOAAAACxkbW5kAAABjAAAAFJkbWRkAAAB4AAAADJt - bHVjAAAAAAAAAAEAAAAMZW5VUwAAAEQAAAAcAEgAdQBlAHkAUABSAE8AIABDAG8AbABv - AHIAIABMAEMARAAgACgARAA2ADUAIABHADIALgAyACAAQQAwAC4AMAAwACltbHVjAAAA - AAAAAAEAAAAMZW5VUwAAADYAAAAcAFgALQBSAGkAdABlACAASQBuAGMALgAgACgAdwB3 - AHcALgB4AHIAaQB0AGUALgBjAG8AbQApAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABYA - AAAcAEMAbwBsAG8AcgAgAEwAQwBEACAAMAAAWFlaIAAAAAAAAG4ZAABCdAAACBZYWVog - AAAAAAAAWr4AAI0oAAAbLFhZWiAAAAAAAAAt/AAAMGIAAK/nY3VydgAAAAAAAAEAAAAA - AAABAAMABwALABEAGAAgACkANABBAE4AXQBuAIAAlACpAMAA2ADyAQ0BKgFJAWkBiwGv - AdQB+wIkAk8CewKpAtkDCgM9A3IDqQPiBBwEWQSXBNcFGQVdBaIF6gYzBn4GywcaB2sH - vggTCGoIwwkdCXoJ2Qo5CpwLAQtnC9AMOgynDRYNhg35Dm4O5Q9eD9kQVhDVEVYR2RJe - EuYTbxP7FIkVGRWqFj8W1RdtGAgYpBlDGeQahxssG9Qcfh0pHdcehx86H+4gpSFeIhki - 1yOWJFglHCXjJqsndihDKRIp5Cq3K44sZi1ALh0u/C/eMMExpzKQM3o0ZzVWNkg3PDgy - OSo6JTsiPCE9Iz4nPy5ANkFBQk9DX0RxRYVGnEe1SNFJ70sPTDJNV05/T6lQ1VIEUzVU - aFWeVtdYEVlOWo5b0F0UXltfpGDwYj5jj2TiZjdnj2jpakZrpW0Hbmtv0nE7cqd0FXWF - dvh4bnnme2B83X5df9+BY4LqhHOF/4eOiR+KsoxIjeGPfJEZkrmUXJYBl6mZU5sAnK+e - YaAVocyjhqVCpwGowqqFrEyuFa/gsa6zf7VStyi5ALrbvLi+mMB7wmDESMYyyCDKD8wB - zfbP7tHo0+XV5Nfm2erb8d374AjiF+Qo5j3oVOpt7InuqPDK8u71Ffc++Wr7mf3K//9j - dXJ2AAAAAAAAAQAAAAAAAAEAAwAHAAsAEQAYACAAKQA0AEEATgBdAG4AgACUAKkAwADY - APIBDQEqAUkBaQGLAa8B1AH7AiQCTwJ7AqkC2QMKAz0DcgOpA+IEHARZBJcE1wUZBV0F - ogXqBjMGfgbLBxoHawe+CBMIagjDCR0JegnZCjkKnAsBC2cL0Aw6DKcNFg2GDfkObg7l - D14P2RBWENURVhHZEl4S5hNvE/sUiRUZFaoWPxbVF20YCBikGUMZ5BqHGywb1Bx+HSkd - 1x6HHzof7iClIV4iGSLXI5YkWCUcJeMmqyd2KEMpEinkKrcrjixmLUAuHS78L94wwTGn - MpAzejRnNVY2SDc8ODI5KjolOyI8IT0jPic/LkA2QUFCT0NfRHFFhUacR7VI0UnvSw9M - Mk1XTn9PqVDVUgRTNVRoVZ5W11gRWU5ajlvQXRReW1+kYPBiPmOPZOJmN2ePaOlqRmul - bQdua2/ScTtyp3QVdYV2+HhueeZ7YHzdfl1/34FjguqEc4X/h46JH4qyjEiN4Y98kRmS - uZRclgGXqZlTmwCcr55hoBWhzKOGpUKnAajCqoWsTK4Vr+CxrrN/tVK3KLkAutu8uL6Y - wHvCYMRIxjLIIMoPzAHN9s/u0ejT5dXk1+bZ6tvx3fvgCOIX5CjmPehU6m3sie6o8Mry - 7vUV9z75avuZ/cr//2N1cnYAAAAAAAABAAAAAAAAAQADAAcACwARABgAIAApADQAQQBO - AF0AbgCAAJQAqQDAANgA8gENASoBSQFpAYsBrwHUAfsCJAJPAnsCqQLZAwoDPQNyA6kD - 4gQcBFkElwTXBRkFXQWiBeoGMwZ+BssHGgdrB74IEwhqCMMJHQl6CdkKOQqcCwELZwvQ - DDoMpw0WDYYN+Q5uDuUPXg/ZEFYQ1RFWEdkSXhLmE28T+xSJFRkVqhY/FtUXbRgIGKQZ - QxnkGocbLBvUHH4dKR3XHocfOh/uIKUhXiIZItcjliRYJRwl4yarJ3YoQykSKeQqtyuO - LGYtQC4dLvwv3jDBMacykDN6NGc1VjZINzw4MjkqOiU7IjwhPSM+Jz8uQDZBQUJPQ19E - cUWFRpxHtUjRSe9LD0wyTVdOf0+pUNVSBFM1VGhVnlbXWBFZTlqOW9BdFF5bX6Rg8GI+ - Y49k4mY3Z49o6WpGa6VtB25rb9JxO3KndBV1hXb4eG555ntgfN1+XX/fgWOC6oRzhf+H - jokfirKMSI3hj3yRGZK5lFyWAZepmVObAJyvnmGgFaHMo4alQqcBqMKqhaxMrhWv4LGu - s3+1UrcouQC627y4vpjAe8JgxEjGMsggyg/MAc32z+7R6NPl1eTX5tnq2/Hd++AI4hfk - KOY96FTqbeyJ7qjwyvLu9RX3Pvlq+5n9yv//WFlaIAAAAAAAAPbVAAEAAAAA0ytYWVog - AAAAAAAAAHoAAAB+AAAAaG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAagAAABwAQwBvAHAA - eQByAGkAZwBoAHQAIAAoAGMAKQAgAFgALQBSAGkAdABlACwAIAAyADAAMAAxAC0AMgAw - ADAANwAuACAAQQBsAGwAIABSAGkAZwBoAHQAcwAgAFIAZQBzAGUAcgB2AGUAZAAuAAB2 - Y2d0AAAAAAAAAAAAAwEAAAIAAAFtAtkERgWyBx8Iiwn4C2QM0Q49D6oRFhKDE+8VXBZh - F2cYbBlyGncbfRyCHYgejR+TIJkhniKkI6kkryWWJn0nZShMKTMqGysCK+ks0S24Lp8v - hzBuMVUyPTMmNBA0+jXjNs03tzigOYo6cztdPEc9MD4aPwQ/7UDsQetC6UPoROdF5Ubk - R+JI4UngSt5L3UzcTdpO2U/qUPpSC1McVCxVPVZOV15Yb1mAWpBboVyyXcJe01/gYOxh - +WMFZBJlHmYrZzdoRGlQal1ramx2bYNuj2+lcLtx0XLmc/x1EnYodz54U3lpen97lXyr - fcB+1n/SgM+By4LHg8OEwIW8hriHtIiwia2KqYuljKGNno6Cj2aQSpEvkhOS95PclMCV - pJaJl22YUZk1mhqa/pvcnLqdl551n1OgMKEOoeyiyqOnpIWlY6ZBpx6n/Kjdqb2qnat+ - rF6tP64frv+v4LDAsaGygbNitEK1IrYCtuG3wLifuX66Xbs8vBu8+r3Zvri/l8B2wVbC - NcMHw9nErMV+xlDHI8f1yMfJmcpsyz7MEMzjzbXOh89E0ALQv9F80jnS9tOz1HDVLdXq - 1qfXZdgi2N/ZnNpP2wHbtNxn3Rrdzd6A3zLf5eCY4Uvh/uKw42PkFuS+5WbmDea1513o - BOis6VTp/Oqj60vr8+ya7ULt6gAAAS4CWwOJBLcF5AcSCEAJbQqbC8kM9g4kD1IQgBGt - EogTYxQ+FRkV9BbOF6kYhBlfGjobFRvwHMsdpR6AHxsftiBRIOwhhyIiIr0jWCPzJI4l - KSXEJl8m+ieVKEIo8CmdKksq+CumLFMtAS2uLlsvCS+2MGQxETG/MnIzJTPYNIs1PjXy - NqU3WDgLOL45cTokOtg7izw+PQQ9yT6PP1VAG0DhQadCbEMyQ/hEvkWERklHD0fVSLBJ - ikplSz9MGkz1Tc9Oqk+EUF9ROVIUUu9TyVSkVY1Wd1dgWElZM1ocWwZb71zZXcJerF+V - YH5haGJRYylkAWTZZbBmiGdgaDhpD2nnar9rl2xvbUZuHm72b8JwjnFZciVy8XO9dIl1 - VHYgdux3uHiEeU96G3rne8l8rH2OfnB/U4A1gReB+oLcg76EoYWDhmWHSIgqiN+JlYpK - iwCLtYxrjSCN1o6Lj0GP9pCskWGSF5LMk4uUSpUIlceWhpdEmAOYwpmAmj+a/pu8nHud - Op34nrOfbaAnoOGhnKJWoxCjyqSFpT+l+aazp26oKKjiqYqqM6rbq4OsK6zUrXyuJK7N - r3WwHbDFsW6yFrK+s2u0F7TEtXC2HbbJt3a4IrjPuXu6KLrUu4G8LbzavYu+Pb7vv6DA - UsEDwbXCZsMYw8nEe8Usxd7Gj8dBAAABPAJ4A7QE8AYsB2gIpAngCxwMWA2UDtAQDBFI - EoQTZxRJFSwWDhbxF9MYthmYGnsbXhxAHSMeBR7oH8ogeCEmIdMigSMvI9wkiiU4JeYm - kydBJ+8onClKKfgqqCtYLAgsuS1pLhkuyS95MCkw2jGKMjoy6jOaNEo1DjXRNpU3WDgc - ON85ozpmOyo77TyxPXQ+Nz77P75AmEFxQktDJEP+RNdFsUaKR2RIPUkXSfBKykujTH1N - c05pT2BQVlFNUkNTOlQwVSdWHVcTWApZAFn3Wu1b21zIXbZepF+RYH9hbWJaY0hkNmUj - ZhFm/2fsaNpp0mrLa8RsvG21bq5vpnCfcZhykHOJdIJ1enZzd2x4bHltem57b3xvfXB+ - cX9ygHKBc4J0g3SEdYV2hneHXYhDiSmKD4r1i9uMwY2njo2Pc5BZkT+SJZMLk/GU1ZW6 - lp6Xg5hnmUuaMJsUm/mc3Z3CnqafiqBvoVOiR6M6pC2lIKYUpwen+qjtqeGq1KvHrLut - rq6hr5SwmbGfsqSzqbSutbO2uLe9uMK5x7rNu9K8173cvuG//MEXwjPDTsRpxYTGn8e7 - yNbJ8csMzCfNQ85ez3nQm9G90t/UAdUj1kbXaNiK2azaztvw3RLeNN9W4HjikuSs5sbo - 4Or67RTvL/FJ82P1ffeX+bH7y/3l//8AAHNmMzIAAAAAAAEN+QAAB+QAAAIBAAAMYwAA - 9SH////2AAABX////RUAARx2 - - ReadOnly - NO - RowAlign - 1 - RowSpacing - 36 - SheetTitle - Canvas 1 - SmartAlignmentGuidesActive - YES - SmartDistanceGuidesActive - YES - UniqueID - 1 - UseEntirePage - - VPages - 1 - WindowInfo - - CurrentSheet - 0 - ExpandedCanvases - - - name - Canvas 1 - - - Frame - {{218, 52}, {999, 826}} - ListView - - OutlineWidth - 142 - RightSidebar - - ShowRuler - - Sidebar - - SidebarWidth - 120 - VisibleRegion - {{-54, -59}, {864, 672}} - Zoom - 1 - ZoomValues - - - Canvas 1 - 1 - 1 - - - - saveQuickLookFiles - YES - - diff --git a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.png b/framework-docs/src/docs/asciidoc/images/oxm-exceptions.png deleted file mode 100644 index 8515e7c4887a..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/prototype.png b/framework-docs/src/docs/asciidoc/images/prototype.png deleted file mode 100644 index 26fa2c1cf2d9..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/prototype.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/singleton.png b/framework-docs/src/docs/asciidoc/images/singleton.png deleted file mode 100644 index 591520ec1dcc..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/singleton.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/spring-mvc-and-webflux-venn.png b/framework-docs/src/docs/asciidoc/images/spring-mvc-and-webflux-venn.png deleted file mode 100644 index 6e0eeab744d4..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/spring-mvc-and-webflux-venn.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/tx.png b/framework-docs/src/docs/asciidoc/images/tx.png deleted file mode 100644 index 06f2e77c76f8..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/tx.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/tx_prop_required.png b/framework-docs/src/docs/asciidoc/images/tx_prop_required.png deleted file mode 100644 index 218790aca635..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/tx_prop_required.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/tx_prop_requires_new.png b/framework-docs/src/docs/asciidoc/images/tx_prop_requires_new.png deleted file mode 100644 index a8ece48193f3..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/tx_prop_requires_new.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/index-docinfo-header.html b/framework-docs/src/docs/asciidoc/index-docinfo-header.html deleted file mode 100644 index 485f2e4e9803..000000000000 --- a/framework-docs/src/docs/asciidoc/index-docinfo-header.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/framework-docs/src/docs/asciidoc/spring-framework.adocbook b/framework-docs/src/docs/asciidoc/spring-framework.adocbook deleted file mode 100644 index e020f7337c2d..000000000000 --- a/framework-docs/src/docs/asciidoc/spring-framework.adocbook +++ /dev/null @@ -1,26 +0,0 @@ -:noheader: -:toc: -:toclevels: 4 -:tabsize: 4 -include::attributes.adoc[] -= Spring Framework Documentation -Rod Johnson; Juergen Hoeller; Keith Donald; Colin Sampaleanu; Rob Harrop; Thomas Risberg; Alef Arendsen; Darren Davison; Dmitriy Kopylenko; Mark Pollack; Thierry Templier; Erwin Vervaet; Portia Tung; Ben Hale; Adrian Colyer; John Lewis; Costin Leau; Mark Fisher; Sam Brannen; Ramnivas Laddad; Arjen Poutsma; Chris Beams; Tareq Abedrabbo; Andy Clement; Dave Syer; Oliver Gierke; Rossen Stoyanchev; Phillip Webb; Rob Winch; Brian Clozel; Stephane Nicoll; Sebastien Deleuze; Jay Bryant; Mark Paluch - -NOTE: This documentation is also available in {docs-spring-framework}/reference/html/index.html[HTML] format. - -[[legal]] -== Legal - -Copyright © 2002 - 2023 VMware, Inc. All Rights Reserved. - -Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. - -include::overview.adoc[leveloffset=+1] -include::core.adoc[leveloffset=+1] -include::testing.adoc[leveloffset=+1] -include::data-access.adoc[leveloffset=+1] -include::web.adoc[leveloffset=+1] -include::web-reactive.adoc[leveloffset=+1] -include::integration.adoc[leveloffset=+1] -include::languages.adoc[leveloffset=+1] -include::appendix.adoc[leveloffset=+1] diff --git a/framework-docs/src/docs/dist/license.txt b/framework-docs/src/docs/dist/license.txt index 89cf3d232fa7..a5fb64294f36 100644 --- a/framework-docs/src/docs/dist/license.txt +++ b/framework-docs/src/docs/dist/license.txt @@ -263,10 +263,10 @@ JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of which is included above. ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): +>>> Objenesis 3.4 (org.objenesis:objenesis:3.4): Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +http://objenesis.org/download.html, Objenesis 3.4 is licensed under the Apache License, version 2.0, the text of which is included above. Per the NOTICE file in the Objenesis ZIP distribution downloaded from diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java index 6151f9cd3782..cd4d87731b60 100644 --- a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class SpellCheckServiceTests { +class SpellCheckServiceTests { // tag::hintspredicates[] @Test diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.java new file mode 100644 index 000000000000..bdcfc4b23233 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes; + +import java.sql.Connection; +import java.sql.SQLException; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import oracle.jdbc.driver.OracleConnection; + +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.support.AbstractSqlTypeValue; + +@SuppressWarnings("unused") +class SqlTypeValueFactory { + + void createStructSample() throws ParseException { + // tag::struct[] + TestItem testItem = new TestItem(123L, "A test item", + new SimpleDateFormat("yyyy-M-d").parse("2010-12-31")); + + SqlTypeValue value = new AbstractSqlTypeValue() { + protected Object createTypeValue(Connection connection, int sqlType, String typeName) throws SQLException { + Object[] item = new Object[] { testItem.getId(), testItem.getDescription(), + new java.sql.Date(testItem.getExpirationDate().getTime()) }; + return connection.createStruct(typeName, item); + } + }; + // end::struct[] + } + + void createOracleArray() { + // tag::oracle-array[] + Long[] ids = new Long[] {1L, 2L}; + + SqlTypeValue value = new AbstractSqlTypeValue() { + protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { + return conn.unwrap(OracleConnection.class).createOracleArray(typeName, ids); + } + }; + // end::oracle-array[] + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.java new file mode 100644 index 000000000000..b0968b877f8f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes; + +import java.util.Date; + +class TestItem { + + private Long id; + + private String description; + + private Date expirationDate; + + public TestItem() { + } + + public TestItem(Long id, String description, Date expirationDate) { + this.id = id; + this.description = description; + this.expirationDate = expirationDate; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getExpirationDate() { + return this.expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.java new file mode 100644 index 000000000000..1343dcfbaee0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes; + +import java.sql.CallableStatement; +import java.sql.Struct; +import java.sql.Types; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.object.StoredProcedure; + +@SuppressWarnings("unused") +public class TestItemStoredProcedure extends StoredProcedure { + + public TestItemStoredProcedure(DataSource dataSource) { + super(dataSource, "get_item"); + declareParameter(new SqlOutParameter("item", Types.STRUCT, "ITEM_TYPE", + (CallableStatement cs, int colIndx, int sqlType, String typeName) -> { + Struct struct = (Struct) cs.getObject(colIndx); + Object[] attr = struct.getAttributes(); + TestItem item = new TestItem(); + item.setId(((Number) attr[0]).longValue()); + item.setDescription((String) attr[1]); + item.setExpirationDate((java.util.Date) attr[2]); + return item; + })); + // ... + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/ApplicationEventsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/ApplicationEventsConfiguration.java new file mode 100644 index 000000000000..bdcf1b152efc --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/ApplicationEventsConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.applicationevents; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; + +@Configuration +public class ApplicationEventsConfiguration { + + @Bean(name = "applicationEventMulticaster") + public SimpleApplicationEventMulticaster simpleApplicationEventMulticaster() { + SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + // decorate task execution with a decorator that supports context propagation + taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator()); + eventMulticaster.setTaskExecutor(taskExecutor); + return eventMulticaster; + } + +} 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 new file mode 100644 index 000000000000..e115bdddc4b8 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailNotificationListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.applicationevents; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +public class EmailNotificationListener { + + private final Log logger = LogFactory.getLog(EmailNotificationListener.class); + + @EventListener(EmailReceivedEvent.class) + @Async("propagatingContextExecutor") + public void emailReceived(EmailReceivedEvent event) { + // asynchronously process the received event + // this logging statement will contain the expected MDC entries from the propagated context + logger.info("email has been received"); + } + +} 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 new file mode 100644 index 000000000000..1e9e27b52cbb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailReceivedEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.applicationevents; + +import org.springframework.context.ApplicationEvent; + +@SuppressWarnings("serial") +public class EmailReceivedEvent extends ApplicationEvent { + + public EmailReceivedEvent(Object source) { + super(source); + } + +} 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 new file mode 100644 index 000000000000..4c07472bd5d0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EventAsyncExecutionConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.applicationevents; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; + +@Configuration +public class EventAsyncExecutionConfiguration { + + @Bean(name = "propagatingContextExecutor") + public TaskExecutor propagatingContextExecutor() { + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + // decorate task execution with a decorator that supports context propagation + taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator()); + return taskExecutor; + } + +} 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 dc76cbc8395f..44000cd41bf9 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ private KeyValue status(ServerRequestObservationContext context) { } private KeyValue exception(ServerRequestObservationContext context) { - String exception = (context.getError() != null) ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE; + String exception = (context.getError() != null ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE); return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, exception); } 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 new file mode 100644 index 000000000000..dab8da25d5c0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/HttpHandlerConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.httpserver.reactive; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +@Configuration(proxyBeanMethods = false) +public class HttpHandlerConfiguration { + + private final ApplicationContext applicationContext; + + public HttpHandlerConfiguration(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Bean + public HttpHandler httpHandler(ObservationRegistry registry) { + return WebHttpHandlerBuilder.applicationContext(this.applicationContext) + .observationRegistry(registry) + .build(); + } +} 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 0f7d8b68a664..67b035f7f3e6 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 @@ -17,9 +17,9 @@ package org.springframework.docs.integration.observability.httpserver.reactive; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.observation.ServerRequestObservationContext; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.filter.reactive.ServerHttpObservationFilter; import org.springframework.web.server.ServerWebExchange; @Controller @@ -28,7 +28,7 @@ public class UserController { @ExceptionHandler(MissingUserException.class) ResponseEntity handleMissingUser(ServerWebExchange exchange, MissingUserException exception) { // We want to record this exception with the observation - ServerHttpObservationFilter.findObservationContext(exchange) + ServerRequestObservationContext.findCurrent(exchange.getAttributes()) .ifPresent(context -> context.setError(exception)); return ResponseEntity.notFound().build(); } 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 new file mode 100644 index 000000000000..d611d3ed8153 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/process/JmsConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.jms.process; + +import io.micrometer.observation.ObservationRegistry; +import jakarta.jms.ConnectionFactory; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; + +@Configuration +@EnableJms +public class JmsConfiguration { + + @Bean + public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, ObservationRegistry observationRegistry) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + factory.setObservationRegistry(observationRegistry); + return factory; + } + +} 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 new file mode 100644 index 000000000000..4cc828714f7c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/publish/JmsTemplatePublish.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.jms.publish; + +import io.micrometer.observation.ObservationRegistry; +import jakarta.jms.ConnectionFactory; + +import org.springframework.jms.core.JmsMessagingTemplate; +import org.springframework.jms.core.JmsTemplate; + +public class JmsTemplatePublish { + + private final JmsTemplate jmsTemplate; + + private final JmsMessagingTemplate jmsMessagingTemplate; + + public JmsTemplatePublish(ObservationRegistry observationRegistry, ConnectionFactory connectionFactory) { + this.jmsTemplate = new JmsTemplate(connectionFactory); + // configure the observation registry + this.jmsTemplate.setObservationRegistry(observationRegistry); + + // For JmsMessagingTemplate, instantiate it with a JMS template that has a configured registry + this.jmsMessagingTemplate = new JmsMessagingTemplate(this.jmsTemplate); + } + + public void sendMessages() { + this.jmsTemplate.convertAndSend("spring.observation.test", "test message"); + } + +} 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 new file mode 100644 index 000000000000..931f362890dc --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/tasksscheduled/ObservationSchedulingConfigurer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.tasksscheduled; + + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +public class ObservationSchedulingConfigurer implements SchedulingConfigurer { + + private final ObservationRegistry observationRegistry; + + public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setObservationRegistry(this.observationRegistry); + } + +} 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..9048f6333435 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.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.EnableWebFlux; +import org.springframework.web.reactive.config.PathMatchConfigurer; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +@Configuration +@EnableWebFlux +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/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.kt new file mode 100644 index 000000000000..808f57895fe8 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes + +import oracle.jdbc.driver.OracleConnection +import org.springframework.jdbc.core.SqlTypeValue +import org.springframework.jdbc.core.support.AbstractSqlTypeValue +import java.sql.Connection +import java.sql.Date +import java.text.SimpleDateFormat + +@Suppress("unused") +class SqlTypeValueFactory { + + fun createStructSample(): AbstractSqlTypeValue { + // tag::struct[] + val testItem = TestItem(123L, "A test item", + SimpleDateFormat("yyyy-M-d").parse("2010-12-31")) + + val value = object : AbstractSqlTypeValue() { + override fun createTypeValue(connection: Connection, sqlType: Int, typeName: String?): Any { + val item = arrayOf(testItem.id, testItem.description, + Date(testItem.expirationDate.time)) + return connection.createStruct(typeName, item) + } + } + // end::struct[] + return value + } + + fun createOracleArray() : SqlTypeValue { + // tag::oracle-array[] + val ids = arrayOf(1L, 2L) + val value: SqlTypeValue = object : AbstractSqlTypeValue() { + override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { + return conn.unwrap(OracleConnection::class.java).createOracleArray(typeName, ids) + } + } + // end::oracle-array[] + return value + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt new file mode 100644 index 000000000000..ac0c648ba6f7 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes + +import org.springframework.jdbc.core.SqlOutParameter +import org.springframework.jdbc.`object`.StoredProcedure +import java.sql.CallableStatement +import java.sql.Struct +import java.sql.Types +import java.util.Date +import javax.sql.DataSource + +@Suppress("unused") +class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, "get_item") { + init { + declareParameter(SqlOutParameter("item",Types.STRUCT,"ITEM_TYPE") { + cs: CallableStatement, colIndx: Int, _: Int, _: String? -> + val struct = cs.getObject(colIndx) as Struct + val attr = struct.attributes + val item = TestItem() + item.id = (attr[0] as Number).toLong() + item.description = attr[1] as String + item.expirationDate = attr[2] as Date + item + }) + // ... + } +} \ 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..b656ef7f5eb6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.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.EnableWebFlux +import org.springframework.web.reactive.config.PathMatchConfigurer +import org.springframework.web.reactive.config.WebFluxConfigurer + +@Configuration +@EnableWebFlux +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-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 8d0ce081ac88..6e3cf4027365 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -7,32 +7,34 @@ javaPlatform { } dependencies { - api(platform("com.fasterxml.jackson:jackson-bom:2.14.3")) - api(platform("io.micrometer:micrometer-bom:1.10.13")) - api(platform("io.netty:netty-bom:4.1.101.Final")) + api(platform("com.fasterxml.jackson:jackson-bom:2.15.4")) + api(platform("io.micrometer:micrometer-bom:1.12.10")) + api(platform("io.netty:netty-bom:4.1.113.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.13")) + api(platform("io.projectreactor:reactor-bom:2023.0.10")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.15")) + api(platform("org.apache.groovy:groovy-bom:4.0.22")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) - api(platform("org.eclipse.jetty:jetty-bom:11.0.18")) - api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) - api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) - api(platform("org.junit:junit-bom:5.9.3")) - api(platform("org.mockito:mockito-bom:5.7.0")) + api(platform("org.assertj:assertj-bom:3.26.3")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.13")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.13")) + api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) + api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) + api(platform("org.junit:junit-bom:5.10.3")) + api(platform("org.mockito:mockito-bom:5.12.0")) constraints { api("com.fasterxml:aalto-xml:1.3.2") - api("com.fasterxml.woodstox:woodstox-core:6.5.1") + api("com.fasterxml.woodstox:woodstox-core:6.6.2") api("com.github.ben-manes.caffeine:caffeine:3.1.8") - api("com.github.librepdf:openpdf:1.3.33") + 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.23.2") - api("com.googlecode.protobuf-java-format:protobuf-java-format:1.4") + api("com.google.protobuf:protobuf-java-util:3.25.3") api("com.h2database:h2:2.2.224") - api("com.jayway.jsonpath:json-path:2.8.0") + api("com.jayway.jsonpath:json-path:2.9.0") + 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") @@ -41,11 +43,11 @@ dependencies { api("com.sun.xml.bind:jaxb-core:3.0.2") api("com.sun.xml.bind:jaxb-impl:3.0.2") api("com.sun.xml.bind:jaxb-xjc:3.0.2") - api("com.thoughtworks.qdox:qdox:2.0.3") + api("com.thoughtworks.qdox:qdox:2.1.0") api("com.thoughtworks.xstream:xstream:1.4.20") api("commons-io:commons-io:2.15.0") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") - api("io.micrometer:context-propagation:1.0.6") + api("io.micrometer:context-propagation:1.1.1") api("io.mockk:mockk:1.13.4") api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3") api("io.projectreactor.tools:blockhound:1.0.8.RELEASE") @@ -53,10 +55,10 @@ dependencies { 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.smallrye.reactive:mutiny:1.9.0") - api("io.undertow:undertow-core:2.3.10.Final") - api("io.undertow:undertow-servlet:2.3.10.Final") - api("io.undertow:undertow-websockets-jsr:2.3.10.Final") + api("io.smallrye.reactive:mutiny:1.10.0") + api("io.undertow:undertow-core:2.3.17.Final") + api("io.undertow:undertow-servlet:2.3.17.Final") + api("io.undertow:undertow-websockets-jsr:2.3.17.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") api("jakarta.annotation:jakarta.annotation-api:2.0.0") @@ -83,6 +85,7 @@ dependencies { 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") @@ -92,31 +95,33 @@ dependencies { api("org.apache.activemq:activemq-broker:5.17.6") api("org.apache.activemq:activemq-kahadb-store:5.17.6") api("org.apache.activemq:activemq-stomp:5.17.6") + api("org.apache.activemq:artemis-jakarta-client:2.31.2") + api("org.apache.activemq:artemis-junit-5:2.31.2") api("org.apache.commons:commons-pool2:2.9.0") api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") - api("org.apache.httpcomponents.client5:httpclient5:5.2.1") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.3") - api("org.apache.poi:poi-ooxml:5.2.4") - api("org.apache.tomcat.embed:tomcat-embed-core:10.1.15") - api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.15") - api("org.apache.tomcat:tomcat-util:10.1.15") - api("org.apache.tomcat:tomcat-websocket:10.1.15") - api("org.aspectj:aspectjrt:1.9.20.1") - api("org.aspectj:aspectjtools:1.9.20.1") - api("org.aspectj:aspectjweaver:1.9.20.1") - api("org.assertj:assertj-core:3.24.2") + api("org.apache.httpcomponents.client5:httpclient5:5.3.1") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.5") + api("org.apache.poi:poi-ooxml:5.2.5") + api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28") + api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28") + api("org.apache.tomcat:tomcat-util:10.1.28") + api("org.apache.tomcat:tomcat-websocket:10.1.28") + api("org.aspectj:aspectjrt:1.9.22.1") + api("org.aspectj:aspectjtools:1.9.22.1") + api("org.aspectj:aspectjweaver:1.9.22.1") api("org.awaitility:awaitility:4.2.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:3.0.10") + api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.7") api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4") api("org.eclipse:yasson:2.0.4") api("org.ehcache:ehcache:3.10.8") api("org.ehcache:jcache:1.0.1") - api("org.freemarker:freemarker:2.3.32") + api("org.freemarker:freemarker:2.3.33") 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") @@ -125,22 +130,22 @@ dependencies { api("org.hibernate:hibernate-core-jakarta:5.6.15.Final") api("org.hibernate:hibernate-validator:7.0.5.Final") api("org.hsqldb:hsqldb:2.7.2") - api("org.javamoney:moneta:1.4.2") - api("org.jruby:jruby:9.4.5.0") - api("org.junit.support:testng-engine:1.0.4") - api("org.mozilla:rhino:1.7.14") + api("org.javamoney:moneta:1.4.4") + api("org.jruby:jruby:9.4.8.0") + api("org.junit.support:testng-engine:1.0.5") + api("org.mozilla:rhino:1.7.15") api("org.ogce:xpp3:1.1.6") api("org.python:jython-standalone:2.7.3") api("org.quartz-scheduler:quartz:2.3.2") api("org.seleniumhq.selenium:htmlunit-driver:2.70.0") api("org.seleniumhq.selenium:selenium-java:3.141.59") - api("org.skyscreamer:jsonassert:1.5.1") - api("org.slf4j:slf4j-api:2.0.9") - api("org.testng:testng:7.8.0") + api("org.skyscreamer:jsonassert:1.5.3") + api("org.slf4j:slf4j-api:2.0.16") + api("org.testng:testng:7.9.0") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-core:0.55") api("org.xmlunit:xmlunit-assertj:2.9.1") api("org.xmlunit:xmlunit-matchers:2.9.1") - api("org.yaml:snakeyaml:1.33") + api("org.yaml:snakeyaml:2.2") } } diff --git a/gradle.properties b/gradle.properties index c54c8a26d3a0..45e995bc363d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,10 @@ -version=6.0.14-SNAPSHOT +version=6.1.13 org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -kotlinVersion=1.7.21 +kotlinVersion=1.9.25 +kotlin.jvm.target.validation.mode=ignore kotlin.stdlib.default.dependency=false diff --git a/gradle/docs-dokka.gradle b/gradle/docs-dokka.gradle index 147c39497f2a..7d593bf49de1 100644 --- a/gradle/docs-dokka.gradle +++ b/gradle/docs-dokka.gradle @@ -6,6 +6,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure { classpath.from(sourceSets["main"].runtimeClasspath) externalDocumentationLink { url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/")) + packageListUrl.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/element-list")) } externalDocumentationLink { url.set(new URL("https://projectreactor.io/docs/core/release/api/")) @@ -21,6 +22,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure { } externalDocumentationLink { url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/")) + packageListUrl.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list")) } externalDocumentationLink { url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/")) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 09235f2c61e5..fbfa2c804b95 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -1,3 +1,4 @@ +import org.gradle.plugins.ide.eclipse.model.Library import org.gradle.plugins.ide.eclipse.model.ProjectDependency import org.gradle.plugins.ide.eclipse.model.SourceFolder @@ -6,6 +7,7 @@ apply plugin: 'eclipse' eclipse.jdt { sourceCompatibility = 17 targetCompatibility = 17 + javaRuntimeName = "JavaSE-17" } // Replace classpath entries with project dependencies (GRADLE-1116) @@ -60,12 +62,38 @@ eclipse.classpath.file.whenMerged { } } -// Ensure that JMH sources and resources are treated as test classpath entries -// so that they can see test fixtures. -// https://github.com/melix/jmh-gradle-plugin/issues/157 +// Remove recursive project dependencies eclipse.classpath.file.whenMerged { - entries.findAll { it.path =~ /src\/jmh\/(java|kotlin|resources)/ }.each { - it.entryAttributes['test'] = 'true' + entries.findAll { it instanceof ProjectDependency && it.path == ('/' + project.name) }.each { + entries.remove(it) + } +} + +// 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)\/java21/ } +} + +// 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 { + entries.findAll { it instanceof Library && !file(it.path).exists() }.each { + entries.remove(it) + } +} + +// Due to an apparent bug in Gradle, even though we exclude the "main" classpath +// entries for sources generated by XJC in spring-oxm.gradle, the Gradle eclipse +// plugin still includes them in the generated .classpath file. So, we have to +// manually remove those lingering "main" entries. +if (project.name == "spring-oxm") { + eclipse.classpath.file.whenMerged { classpath -> + classpath.entries.removeAll { + it.path =~ /build\/generated\/sources\/xjc\/.+/ && + it.entryAttributes.get("gradle_scope") == "main" + } } } diff --git a/gradle/publications.gradle b/gradle/publications.gradle index 86e0d2221c0b..db0772caa4f0 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -29,7 +29,7 @@ publishing { developer { id = "jhoeller" name = "Juergen Hoeller" - email = "jhoeller@pivotal.io" + email = "juergen.hoeller@broadcom.com" } } issueManagement { diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 130c00fe6222..b327b0f2f25f 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -3,13 +3,14 @@ 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" dependencies { - jmh 'org.openjdk.jmh:jmh-core:1.36' - jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.36' + jmh 'org.openjdk.jmh:jmh-core:1.37' + jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37' jmh 'net.sf.jopt-simple:jopt-simple' } diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle index 70d23b7e667a..152abb08db45 100644 --- a/gradle/toolchains.gradle +++ b/gradle/toolchains.gradle @@ -2,16 +2,14 @@ * Apply the JVM Toolchain conventions * See https://docs.gradle.org/current/userguide/toolchains.html * - * One can choose the toolchain to use for compiling the MAIN sources and/or compiling - * and running the TEST sources. These options apply to Java, Kotlin and Groovy sources - * when available. - * {@code "./gradlew check -PmainToolchain=17 -PtestToolchain=20"} will use: - *
    - *
  • a JDK17 toolchain for compiling the main SourceSet - *
  • a JDK20 toolchain for compiling and running the test SourceSet - *
+ * 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 build will fall back to using the current JDK and 17 language level for all sourceSets. + * 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. @@ -23,52 +21,27 @@ * {@code * $ echo JDK17 * /opt/openjdk/java17 - * $ echo JDK20 - * /opt/openjdk/java20 - * $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 check + * $ echo JDK22 + * /opt/openjdk/java22 + * $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK22 check * } * * @author Brian Clozel * @author Sam Brannen */ -def mainToolchainConfigured() { - return project.hasProperty('mainToolchain') && project.mainToolchain -} - def testToolchainConfigured() { return project.hasProperty('testToolchain') && project.testToolchain } -def mainToolchainLanguageVersion() { - if (mainToolchainConfigured()) { - return JavaLanguageVersion.of(project.mainToolchain.toString()) - } - return JavaLanguageVersion.of(17) -} - def testToolchainLanguageVersion() { if (testToolchainConfigured()) { return JavaLanguageVersion.of(project.testToolchain.toString()) } - return mainToolchainLanguageVersion() + return JavaLanguageVersion.of(17) } -plugins.withType(JavaPlugin) { - // Configure the Java Toolchain if the 'mainToolchain' is configured - if (mainToolchainConfigured()) { - java { - toolchain { - languageVersion = mainToolchainLanguageVersion() - } - } - } - else { - // Fallback to JDK17 - java { - sourceCompatibility = JavaVersion.VERSION_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() @@ -81,37 +54,18 @@ plugins.withType(JavaPlugin) { javaLauncher = javaToolchains.launcherFor { languageVersion = testLanguageVersion } - jvmArgs += ['-Djava.locale.providers=COMPAT'] - } - } -} - -plugins.withType(GroovyPlugin) { - // Fallback to JDK17 - if (!mainToolchainConfigured()) { - compileGroovy { - sourceCompatibility = JavaVersion.VERSION_17 - } - } -} - -pluginManager.withPlugin("kotlin") { - // Fallback to JDK17 - compileKotlin { - kotlinOptions { - jvmTarget = '17' - } - } - compileTestKotlin { - kotlinOptions { - jvmTarget = '17' + // 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 (mainToolchainConfigured() || testToolchainConfigured()) { + if (testToolchainConfigured()) { tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach { javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(testToolchainLanguageVersion()) @@ -131,14 +85,14 @@ rootProject.ext { resolvedTestToolchain = false } gradle.taskGraph.afterTask { Task task, TaskState state -> - if (mainToolchainConfigured() && !resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) { + if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) { def metadata = task.javaCompiler.get().metadata - task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") + 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.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") + 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 7f93135c49b7..2c3521197d7c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862f753..09523c0e5490 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426907..f5feea6d6b11 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ 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 +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 6689b85beecd..9b42019c7915 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/integration-tests/integration-tests.gradle b/integration-tests/integration-tests.gradle index 88455c9391b7..1444b2bb210b 100644 --- a/integration-tests/integration-tests.gradle +++ b/integration-tests/integration-tests.gradle @@ -1,5 +1,6 @@ plugins { id 'org.springframework.build.runtimehints-agent' + id 'kotlin' } description = "Spring Integration Tests" @@ -26,6 +27,7 @@ dependencies { testImplementation("org.aspectj:aspectjweaver") testImplementation("org.hsqldb:hsqldb") testImplementation("org.hibernate:hibernate-core-jakarta") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") } normalization { diff --git a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java index 683a5d994a06..c711d6e4e75a 100644 --- a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,7 +75,7 @@ void testSingletonScoping() throws Exception { } @Test - void testRequestScoping() throws Exception { + void testRequestScoping() { MockHttpServletRequest oldRequest = new MockHttpServletRequest(); MockHttpServletRequest newRequest = new MockHttpServletRequest(); @@ -103,7 +103,7 @@ void testRequestScoping() throws Exception { } @Test - void testSessionScoping() throws Exception { + void testSessionScoping() { MockHttpSession oldSession = new MockHttpSession(); MockHttpSession newSession = new MockHttpSession(); diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java index 48142f4c9374..15fbd10d787a 100644 --- a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.aop.framework.autoproxy; -import java.io.IOException; import java.lang.reflect.Method; import java.util.List; @@ -61,12 +60,12 @@ class AdvisorAutoProxyCreatorIntegrationTests { /** * Return a bean factory with attributes and EnterpriseServices configured. */ - protected BeanFactory getBeanFactory() throws IOException { + protected BeanFactory getBeanFactory() { return new ClassPathXmlApplicationContext(DEFAULT_CONTEXT, CLASS); } @Test - void testDefaultExclusionPrefix() throws Exception { + void testDefaultExclusionPrefix() { DefaultAdvisorAutoProxyCreator aapc = (DefaultAdvisorAutoProxyCreator) getBeanFactory().getBean(ADVISOR_APC_BEAN_NAME); assertThat(aapc.getAdvisorBeanNamePrefix()).isEqualTo((ADVISOR_APC_BEAN_NAME + DefaultAdvisorAutoProxyCreator.SEPARATOR)); assertThat(aapc.isUsePrefix()).isFalse(); @@ -76,21 +75,21 @@ void testDefaultExclusionPrefix() throws Exception { * If no pointcuts match (no attrs) there should be proxying. */ @Test - void testNoProxy() throws Exception { + void testNoProxy() { BeanFactory bf = getBeanFactory(); Object o = bf.getBean("noSetters"); assertThat(AopUtils.isAopProxy(o)).isFalse(); } @Test - void testTxIsProxied() throws Exception { + void testTxIsProxied() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); assertThat(AopUtils.isAopProxy(test)).isTrue(); } @Test - void testRegexpApplied() throws Exception { + void testRegexpApplied() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); MethodCounter counter = (MethodCounter) bf.getBean("countingAdvice"); @@ -100,7 +99,7 @@ void testRegexpApplied() throws Exception { } @Test - void testTransactionAttributeOnMethod() throws Exception { + void testTransactionAttributeOnMethod() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); @@ -166,7 +165,7 @@ void testRollbackRulesOnMethodPreventRollback() throws Exception { } @Test - void testProgrammaticRollback() throws Exception { + void testProgrammaticRollback() { BeanFactory bf = getBeanFactory(); Object bean = bf.getBean(TXMANAGER_BEAN_NAME); @@ -250,7 +249,7 @@ public CountingBeforeAdvice getCountingBeforeAdvice() { } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { setAdvice(new TxCountingBeforeAdvice()); } diff --git a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java index 2309cbc01d6e..1de66c6612a9 100644 --- a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java +++ b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ * @author Brian Clozel */ @EnabledIfRuntimeHintsAgent -public class RuntimeHintsAgentTests { +class RuntimeHintsAgentTests { private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); @@ -58,7 +58,7 @@ public class RuntimeHintsAgentTests { @BeforeAll - public static void classSetup() throws NoSuchMethodException { + static void classSetup() throws NoSuchMethodException { defaultConstructor = String.class.getConstructor(); toStringMethod = String.class.getMethod("toString"); privateGreetMethod = PrivateClass.class.getDeclaredMethod("greet"); diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java index 31eee877ed5e..613fcb32e9d2 100644 --- a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ class ComponentBeanDefinitionParserTests { @BeforeAll - void setUp() throws Exception { + void setUp() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("component-config.xml", ComponentBeanDefinitionParserTests.class)); } diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java index ec2479cd196f..12255e838860 100644 --- a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ public void setChildren(List children) { } @Override - public Component getObject() throws Exception { + public Component getObject() { if (this.children != null && this.children.size() > 0) { for (Component child : children) { this.parent.addComponent(child); diff --git a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java index 41acb6ed5d62..104ca11ca1e4 100644 --- a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,6 @@ * @author Chris Beams * @since 3.1 */ -@SuppressWarnings("resource") class EnableCachingIntegrationTests { @Test @@ -63,7 +62,7 @@ void repositoryUsesAspectJAdviceMode() { // attempt was made to look up the AJ aspect. It's due to classpath issues // in integration-tests that it's not found. assertThatException().isThrownBy(ctx::refresh) - .withMessageContaining("AspectJCachingConfiguration"); + .withMessageContaining("AspectJCachingConfiguration"); } diff --git a/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java index c059826c8255..adce710f1606 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -344,7 +344,7 @@ public interface IScopedTestBean { } - public static abstract class ScopedTestBean implements IScopedTestBean { + public abstract static class ScopedTestBean implements IScopedTestBean { private String name = DEFAULT_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 c97840f61fb2..504da54de572 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -306,7 +306,7 @@ interface IScopedTestBean { } - static abstract class ScopedTestBean implements IScopedTestBean { + abstract static class ScopedTestBean implements IScopedTestBean { private String name = DEFAULT_NAME; diff --git a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java index 2bbc001f28eb..824099b4c2c6 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,6 @@ * @author Sam Brannen * @see org.springframework.context.support.EnvironmentIntegrationTests */ -@SuppressWarnings("resource") public class EnvironmentSystemIntegrationTests { private final ConfigurableEnvironment prodEnv = new StandardEnvironment(); @@ -542,8 +541,7 @@ void abstractApplicationContextValidatesRequiredPropertiesOnRefresh() { { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setRequiredProperties("foo", "bar"); - assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( - ctx::refresh); + assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(ctx::refresh); } { @@ -618,7 +616,7 @@ public void setEnvironment(Environment environment) { @Import({DevConfig.class, ProdConfig.class}) static class Config { @Bean - public EnvironmentAwareBean envAwareBean() { + EnvironmentAwareBean envAwareBean() { return new EnvironmentAwareBean(); } } diff --git a/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java b/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java index 63174d2d3327..59da59d7ef58 100644 --- a/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java +++ b/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.TypeConverter; +import org.springframework.util.ClassUtils; /** * Copied from Spring Integration for purposes of reproducing @@ -59,11 +60,9 @@ public void setConversionService(ConversionService conversionService) { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - if (beanFactory instanceof ConfigurableBeanFactory) { - Object typeConverter = ((ConfigurableBeanFactory) beanFactory).getTypeConverter(); - if (typeConverter instanceof SimpleTypeConverter) { - delegate = (SimpleTypeConverter) typeConverter; - } + if (beanFactory instanceof ConfigurableBeanFactory cbf && + cbf.getTypeConverter() instanceof SimpleTypeConverter simpleTypeConverter) { + this.delegate = simpleTypeConverter; } } @@ -86,7 +85,6 @@ public boolean canConvert(TypeDescriptor sourceTypeDescriptor, TypeDescriptor ta if (conversionService.canConvert(sourceTypeDescriptor, targetTypeDescriptor)) { return true; } - // TODO: what does this mean? This method is not used in SpEL so probably ignorable? Class sourceType = sourceTypeDescriptor.getObjectType(); Class targetType = targetTypeDescriptor.getObjectType(); return canConvert(sourceType, targetType); @@ -94,7 +92,7 @@ public boolean canConvert(TypeDescriptor sourceTypeDescriptor, TypeDescriptor ta @Override public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (targetType.getType() == Void.class || targetType.getType() == Void.TYPE) { + if (ClassUtils.isVoidType(targetType.getType())) { return null; } if (conversionService.canConvert(sourceType, targetType)) { diff --git a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java index b6f5102491a6..050e3793f7f2 100644 --- a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,6 @@ * @author Juergen Hoeller * @since 3.1 */ -@SuppressWarnings("resource") @EnabledForTestGroups(LONG_RUNNING) class ScheduledAndTransactionalAnnotationIntegrationTests { diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java index 4ab8cab1569e..9b7eef5a0961 100644 --- a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,6 @@ * @author Sam Brannen * @since 3.1 */ -@SuppressWarnings("resource") class EnableTransactionManagementIntegrationTests { @Test @@ -98,9 +97,8 @@ void repositoryUsesAspectJAdviceMode() { // this test is a bit fragile, but gets the job done, proving that an // attempt was made to look up the AJ aspect. It's due to classpath issues // in integration-tests that it's not found. - assertThatException() - .isThrownBy(ctx::refresh) - .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); + assertThatException().isThrownBy(ctx::refresh) + .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); } @Test diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java index 67a61e85fab8..510fc08d92f4 100644 --- a/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,14 +27,13 @@ /** * Tests proving that regardless the proxy strategy used (JDK interface-based vs. CGLIB * subclass-based), discovery of advice-oriented annotations is consistent. - * + *

* For example, Spring's @Transactional may be declared at the interface or class level, * and whether interface or subclass proxies are used, the @Transactional annotation must * be discovered in a consistent fashion. * * @author Chris Beams */ -@SuppressWarnings("resource") class ProxyAnnotationDiscoveryTests { @Test diff --git a/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt b/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt new file mode 100644 index 000000000000..7d32db287b11 --- /dev/null +++ b/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.framework.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 + + +/** + * Integration tests for interceptors with Kotlin (with and without Coroutines) configured + * via AspectJ auto-proxy support. + */ +@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, + @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) + } + 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 + fun `Aspect and reactive transactional with suspending function`() { + assertThat(countingAspect.counter).isZero() + assertThat(reactiveTransactionManager.commits).isZero() + val value = "Hello!" + runBlocking { + assertThat(echo.suspendingTransactionalEcho(value)).isEqualTo(value) + } + assertThat(countingAspect.counter).`as`("aspect applied").isOne() + assertThat(reactiveTransactionManager.begun).isOne() + assertThat(reactiveTransactionManager.commits).`as`("transactional applied").isOne() + } + + @Test // gh-33210 + fun `Aspect and cacheable with suspending function`() { + assertThat(countingAspect.counter).isZero() + val value = "Hello!" + runBlocking { + assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0") + assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0") + assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0") + assertThat(countingAspect.counter).`as`("aspect applied once").isOne() + + assertThat(echo.suspendingCacheableEcho("$value bis")).isEqualTo("$value bis 1") + assertThat(echo.suspendingCacheableEcho("$value bis")).isEqualTo("$value bis 1") + } + assertThat(countingAspect.counter).`as`("aspect applied once per key").isEqualTo(2) + } + + @Configuration + @EnableAspectJAutoProxy + @EnableTransactionManagement + @EnableCaching + open class InterceptorConfig { + + @Bean + open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 } + + @Bean + open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 } + + @Bean + open fun countingAspect() = CountingAspect() + + @Bean + open fun transactionManager(): ReactiveCallCountingTransactionManager { + return ReactiveCallCountingTransactionManager() + } + + @Bean + open fun cacheManager(): CacheManager { + return ConcurrentMapCacheManager() + } + + @Bean + open fun echo(): Echo { + return Echo() + } + } + + class TestMethodInterceptor: MethodInterceptor { + + var invocations: MutableList> = mutableListOf() + + @Suppress("RedundantNullableReturnType") + override fun invoke(invocation: MethodInvocation): Any? { + val result = invocation.proceed() + invocations.add(result!!.javaClass) + return result + } + + } + + class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) { + + val interceptor: TestMethodInterceptor + get() = advice as TestMethodInterceptor + + override fun matches(method: Method, targetClass: Class<*>): Boolean { + return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo") + } + } + + @Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE) + @Retention(AnnotationRetention.RUNTIME) + annotation class Counting() + + @Aspect + @Component + class CountingAspect { + + var counter: Long = 0 + + @Around("@annotation(org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.Counting)") + fun logging(joinPoint: ProceedingJoinPoint): Any { + return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate { + counter++ + }.checkpoint("CountingAspect") + } + } + + open class Echo { + + open fun echo(value: String): String { + return value + } + + open suspend fun suspendingEcho(value: String): String { + delay(1) + return value + } + + @Transactional + @Counting + open suspend fun suspendingTransactionalEcho(value: String): String { + delay(1) + return value + } + + open var cacheCounter: Int = 0 + + @Counting + @Cacheable("something") + open suspend fun suspendingCacheableEcho(value: String): String { + delay(1) + return "$value ${cacheCounter++}" + } + + } + +} diff --git a/settings.gradle b/settings.gradle index fac8d089dec4..3bc6898a5ba3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,14 +1,7 @@ -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - maven { url "https://repo.spring.io/release" } - } -} - plugins { - id "com.gradle.enterprise" version "3.12.6" - id "io.spring.ge.conventions" version "0.0.13" + 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" } include "spring-aop" @@ -34,6 +27,7 @@ include "spring-web" include "spring-webflux" include "spring-webmvc" include "spring-websocket" +include "framework-api" include "framework-bom" include "framework-docs" include "framework-platform" @@ -45,7 +39,7 @@ rootProject.children.each {project -> } settings.gradle.projectsLoaded { - gradleEnterprise { + develocity { buildScan { File buildDir = settings.gradle.rootProject .getLayout().getBuildDirectory().getAsFile().get() diff --git a/spring-aop/spring-aop.gradle b/spring-aop/spring-aop.gradle index 461d933be42d..2e166980450d 100644 --- a/spring-aop/spring-aop.gradle +++ b/spring-aop/spring-aop.gradle @@ -1,14 +1,18 @@ description = "Spring AOP" +apply plugin: "kotlin" + dependencies { api(project(":spring-beans")) api(project(":spring-core")) 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"))) + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") } 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 8ac8f65ca3fa..08b02a502fa2 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-2016 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ * * @author Rod Johnson */ -public interface ConstructorInterceptor extends Interceptor { +public interface ConstructorInterceptor extends Interceptor { /** * Implement this method to perform extra treatments before and diff --git a/spring-aop/src/main/java/org/springframework/aop/TargetSource.java b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java index e894c5cb0a10..c19982f31916 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,10 +49,13 @@ public interface TargetSource extends TargetClassAware { * Will all calls to {@link #getTarget()} return the same object? *

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

The default implementation returns {@code false}. * @return {@code true} if the target is immutable * @see #getTarget */ - boolean isStatic(); + default boolean isStatic() { + return false; + } /** * Return a target instance. Invoked immediately before the @@ -67,9 +70,11 @@ public interface TargetSource extends TargetClassAware { /** * Release the given target object obtained from the * {@link #getTarget()} method, if any. + *

The default implementation is empty. * @param target object obtained from a call to {@link #getTarget()} * @throws Exception if the object can't be released */ - void releaseTarget(Object target) throws Exception; + default void releaseTarget(Object target) throws Exception { + } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index a412214ccd62..bbe397880b10 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 @@ -307,7 +307,7 @@ protected void setReturningNameNoCheck(String name) { this.discoveredReturningType = ClassUtils.forName(name, getAspectClassLoader()); } catch (Throwable ex) { - throw new IllegalArgumentException("Returning name '" + name + + throw new IllegalArgumentException("Returning name '" + name + "' is neither a valid argument name nor the fully-qualified " + "name of a Java type on the classpath. Root cause: " + ex); } @@ -342,7 +342,7 @@ protected void setThrowingNameNoCheck(String name) { this.discoveredThrowingType = ClassUtils.forName(name, getAspectClassLoader()); } catch (Throwable ex) { - throw new IllegalArgumentException("Throwing name '" + name + + throw new IllegalArgumentException("Throwing name '" + name + "' is neither a valid argument name nor the fully-qualified " + "name of a Java type on the classpath. Root cause: " + ex); } 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 c6ee43b2ec26..0b18fefea3ee 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.ObjectInputStream; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; @@ -41,6 +42,7 @@ import org.aspectj.weaver.tools.PointcutParser; import org.aspectj.weaver.tools.PointcutPrimitive; import org.aspectj.weaver.tools.ShadowMatch; +import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; @@ -78,12 +80,15 @@ * @author Juergen Hoeller * @author Ramnivas Laddad * @author Dave Syer + * @author Yanming Zhou * @since 2.0 */ @SuppressWarnings("serial") public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware { + private static final String AJC_MAGIC = "ajc$"; + private static final Set SUPPORTED_PRIMITIVES = Set.of( PointcutPrimitive.EXECUTION, PointcutPrimitive.ARGS, @@ -101,6 +106,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut @Nullable private Class pointcutDeclarationScope; + private boolean aspectCompiledByAjc; + private String[] pointcutParameterNames = new String[0]; private Class[] pointcutParameterTypes = new Class[0]; @@ -114,6 +121,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut @Nullable private transient PointcutExpression pointcutExpression; + private transient boolean pointcutParsingFailed = false; + private transient Map shadowMatchCache = new ConcurrentHashMap<>(32); @@ -130,7 +139,7 @@ public AspectJExpressionPointcut() { * @param paramTypes the parameter types for the pointcut */ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, Class[] paramTypes) { - this.pointcutDeclarationScope = declarationScope; + setPointcutDeclarationScope(declarationScope); if (paramNames.length != paramTypes.length) { throw new IllegalStateException( "Number of pointcut parameter names must match number of pointcut parameter types"); @@ -145,6 +154,7 @@ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, */ public void setPointcutDeclarationScope(Class pointcutDeclarationScope) { this.pointcutDeclarationScope = pointcutDeclarationScope; + this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope); } /** @@ -169,25 +179,30 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public ClassFilter getClassFilter() { - obtainPointcutExpression(); + checkExpression(); return this; } @Override public MethodMatcher getMethodMatcher() { - obtainPointcutExpression(); + checkExpression(); return this; } /** - * Check whether this pointcut is ready to match, - * lazily building the underlying AspectJ pointcut expression. + * Check whether this pointcut is ready to match. */ - private PointcutExpression obtainPointcutExpression() { + private void checkExpression() { if (getExpression() == null) { throw new IllegalStateException("Must set property 'expression' before attempting to match"); } + } + + /** + * Lazily build the underlying AspectJ pointcut expression. + */ + private PointcutExpression obtainPointcutExpression() { if (this.pointcutExpression == null) { this.pointcutClassLoader = determinePointcutClassLoader(); this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); @@ -264,10 +279,18 @@ public PointcutExpression getPointcutExpression() { @Override public boolean matches(Class targetClass) { - PointcutExpression pointcutExpression = obtainPointcutExpression(); + if (this.pointcutParsingFailed) { + // Pointcut parsing failed before below -> avoid trying again. + return false; + } + if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) { + // ajc-compiled aspect class for ajc-compiled target class -> already weaved. + return false; + } + try { try { - return pointcutExpression.couldMatchJoinPointsInType(targetClass); + return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass); } catch (ReflectionWorldException ex) { logger.debug("PointcutExpression matching rejected target class - trying fallback expression", ex); @@ -278,6 +301,12 @@ public boolean matches(Class targetClass) { } } } + catch (IllegalArgumentException | IllegalStateException | UnsupportedPointcutPrimitiveException ex) { + this.pointcutParsingFailed = true; + if (logger.isDebugEnabled()) { + logger.debug("Pointcut parser rejected expression [" + getExpression() + "]: " + ex); + } + } catch (Throwable ex) { logger.debug("PointcutExpression matching rejected target class", ex); } @@ -286,7 +315,6 @@ public boolean matches(Class targetClass) { @Override public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { - obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Special handling for this, target, @this, @target, @annotation @@ -324,7 +352,6 @@ public boolean isRuntime() { @Override public boolean matches(Method method, Class targetClass, Object... args) { - obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target, @@ -333,13 +360,15 @@ public boolean matches(Method method, Class targetClass, Object... args) { Object targetObject = null; Object thisObject = null; try { - MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); - targetObject = mi.getThis(); - if (!(mi instanceof ProxyMethodInvocation _pmi)) { - throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + MethodInvocation curr = ExposeInvocationInterceptor.currentInvocation(); + if (curr.getMethod() == method) { + targetObject = curr.getThis(); + if (!(curr instanceof ProxyMethodInvocation currPmi)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + curr); + } + pmi = currPmi; + thisObject = pmi.getProxy(); } - pmi = _pmi; - thisObject = pmi.getProxy(); } catch (IllegalStateException ex) { // No current invocation... @@ -471,10 +500,11 @@ private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { } } if (targetMethod != originalMethod && (shadowMatch == null || - (shadowMatch.neverMatches() && Proxy.isProxyClass(targetMethod.getDeclaringClass())))) { + (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). + // redeclared methods), as well as for annotation pointcuts. methodToMatch = originalMethod; try { shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); @@ -513,6 +543,20 @@ else if (shadowMatch.maybeMatches() && fallbackExpression != null) { return shadowMatch; } + private boolean containsAnnotationPointcut() { + return resolveExpression().contains("@annotation"); + } + + private static boolean compiledByAjc(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + Class superclass = clazz.getSuperclass(); + return (superclass != null && compiledByAjc(superclass)); + } + @Override public boolean equals(@Nullable Object other) { @@ -525,11 +569,8 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(getExpression()); - hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutDeclarationScope); - hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterNames); - hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterTypes); - return hashCode; + return ObjectUtils.nullSafeHash(getExpression(), this.pointcutDeclarationScope, + this.pointcutParameterNames, this.pointcutParameterTypes); } @Override 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 2bb7ab237ab4..543146243ab0 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 @@ -25,8 +25,8 @@ import org.springframework.util.Assert; /** - * AspectJ {@link PointcutAdvisor} that adapts an {@link AbstractAspectJAdvice} - * to the {@link org.springframework.aop.PointcutAdvisor} interface. + * {@code AspectJPointcutAdvisor} adapts an {@link AbstractAspectJAdvice} to the + * {@link PointcutAdvisor} interface. * * @author Adrian Colyer * @author Juergen Hoeller 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 ef9016a15728..68eb55c9c4a6 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-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -291,7 +291,7 @@ private void appendTypes(StringBuilder sb, Class[] types, boolean includeArgs private void appendType(StringBuilder sb, Class type, boolean useLongTypeName) { if (type.isArray()) { - appendType(sb, type.getComponentType(), useLongTypeName); + appendType(sb, type.componentType(), useLongTypeName); sb.append("[]"); } else { 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 bc6de396395a..d6ddae267195 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 @@ -16,6 +16,8 @@ package org.springframework.aop.aspectj; +import java.util.Objects; + import org.aspectj.weaver.tools.PointcutParser; import org.aspectj.weaver.tools.TypePatternMatcher; @@ -124,7 +126,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.typePattern); + return Objects.hashCode(this.typePattern); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java index bf2eb3e45056..6f0eef820701 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ 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; @@ -56,8 +55,6 @@ */ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory { - private static final String AJC_MAGIC = "ajc$"; - private static final Class[] ASPECTJ_ANNOTATION_CLASSES = new Class[] { Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class}; @@ -68,37 +65,11 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac protected final ParameterNameDiscoverer parameterNameDiscoverer = new AspectJAnnotationParameterNameDiscoverer(); - /** - * We consider something to be an AspectJ aspect suitable for use by the Spring AOP system - * if it has the @Aspect annotation, and was not compiled by ajc. The reason for this latter test - * is that aspects written in the code-style (AspectJ language) also have the annotation present - * when compiled by ajc with the -1.5 flag, yet they cannot be consumed by Spring AOP. - */ @Override public boolean isAspect(Class clazz) { - return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); - } - - private boolean hasAspectAnnotation(Class clazz) { return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); } - /** - * We need to detect this as "code-style" AspectJ aspects should not be - * interpreted by Spring AOP. - */ - private boolean compiledByAjc(Class clazz) { - // The AJTypeSystem goes to great lengths to provide a uniform appearance between code-style and - // annotation-style aspects. Therefore there is no 'clean' way to tell them apart. Here we rely on - // an implementation detail of the AspectJ compiler. - for (Field field : clazz.getDeclaredFields()) { - if (field.getName().startsWith(AJC_MAGIC)) { - return true; - } - } - return false; - } - @Override public void validate(Class aspectClass) throws AopConfigException { AjType ajType = AjTypeSystem.getAjType(aspectClass); @@ -115,6 +86,7 @@ public void validate(Class aspectClass) throws AopConfigException { } } + /** * Find and return the first AspectJ annotation on the given method * (there should only be one anyway...). diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java new file mode 100644 index 000000000000..7149816f5742 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.aspectj.annotation; + +import java.lang.reflect.Field; + +import org.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; + +/** + * An AOT {@link BeanRegistrationAotProcessor} that detects the presence of + * classes compiled with AspectJ and adds the related required field hints. + * + * @author Sebastien Deleuze + * @since 6.1 + */ +class AspectJAdvisorBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + private static final String AJC_MAGIC = "ajc$"; + + private static final boolean aspectjPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", + AspectJAdvisorBeanRegistrationAotProcessor.class.getClassLoader()); + + + @Override + @Nullable + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + if (aspectjPresent) { + Class beanClass = registeredBean.getBeanClass(); + if (compiledByAjc(beanClass)) { + return new AspectJAdvisorContribution(beanClass); + } + } + return null; + } + + private static boolean compiledByAjc(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + return false; + } + + + private static class AspectJAdvisorContribution implements BeanRegistrationAotContribution { + + private final Class beanClass; + + public AspectJAdvisorContribution(Class beanClass) { + this.beanClass = beanClass; + } + + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + generationContext.getRuntimeHints().reflection().registerType(this.beanClass, MemberCategory.DECLARED_FIELDS); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java index 969ec866eebb..a70aed625cc9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,10 +124,16 @@ public AspectMetadata(Class aspectClass, String aspectName) { * Extract contents from String of form {@code pertarget(contents)}. */ private String findPerClause(Class aspectClass) { - String str = aspectClass.getAnnotation(Aspect.class).value(); - int beginIndex = str.indexOf('(') + 1; - int endIndex = str.length() - 1; - return str.substring(beginIndex, endIndex); + Aspect ann = aspectClass.getAnnotation(Aspect.class); + if (ann == null) { + return ""; + } + String value = ann.value(); + int beginIndex = value.indexOf('('); + if (beginIndex < 0) { + return ""; + } + return value.substring(beginIndex + 1, value.length() - 1); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java index 8896f990ecbb..a318ea56bb49 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,12 @@ 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.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; @@ -40,6 +43,8 @@ */ public class BeanFactoryAspectJAdvisorsBuilder { + private static final Log logger = LogFactory.getLog(BeanFactoryAspectJAdvisorsBuilder.class); + private final ListableBeanFactory beanFactory; private final AspectJAdvisorFactory advisorFactory; @@ -102,30 +107,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)); } } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java index 89a77213e080..07df51fb3f55 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -293,7 +293,7 @@ public boolean matches(Method method, Class targetClass) { @Override public boolean matches(Method method, Class targetClass, Object... args) { // This can match only on declared pointcut. - return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass)); + return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass, args)); } private boolean isAspectMaterialized() { diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index 4e2f5c3bd801..e4eec7a919d9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConvertingComparator; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; @@ -133,17 +134,19 @@ public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstan List advisors = new ArrayList<>(); for (Method method : getAdvisorMethods(aspectClass)) { - // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect - // to getAdvisor(...) to represent the "current position" in the declared methods list. - // However, since Java 7 the "current position" is not valid since the JDK no longer - // returns declared methods in the order in which they are declared in the source code. - // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods - // discovered via reflection in order to support reliable advice ordering across JVM launches. - // Specifically, a value of 0 aligns with the default value used in - // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). - Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); - if (advisor != null) { - advisors.add(advisor); + if (method.equals(ClassUtils.getMostSpecificMethod(method, aspectClass))) { + // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect + // to getAdvisor(...) to represent the "current position" in the declared methods list. + // However, since Java 7 the "current position" is not valid since the JDK no longer + // returns declared methods in the order in which they are declared in the source code. + // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods + // discovered via reflection in order to support reliable advice ordering across JVM launches. + // Specifically, a value of 0 aligns with the default value used in + // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); + if (advisor != null) { + advisors.add(advisor); + } } } @@ -210,8 +213,16 @@ public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInsta return null; } - return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, - this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + try { + return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, + this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + } + catch (IllegalArgumentException | IllegalStateException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex); + } + return null; + } } @Nullable 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 2d4360048cf7..93540fe11ddb 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-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +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/ConfigBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java index 0a40981134ef..f140b60c88db 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 @@ -201,9 +201,8 @@ private void parseAspect(Element aspectElement, ParserContext parserContext) { List beanReferences = new ArrayList<>(); List declareParents = DomUtils.getChildElementsByTagName(aspectElement, DECLARE_PARENTS); - for (int i = METHOD_INDEX; i < declareParents.size(); i++) { - Element declareParentsElement = declareParents.get(i); - beanDefinitions.add(parseDeclareParents(declareParentsElement, parserContext)); + for (Element declareParent : declareParents) { + beanDefinitions.add(parseDeclareParents(declareParent, parserContext)); } // We have to parse "advice" and all the advice kinds in one loop, to get the @@ -405,24 +404,14 @@ else if (pointcut instanceof String beanName) { */ private Class getAdviceClass(Element adviceElement, ParserContext parserContext) { String elementName = parserContext.getDelegate().getLocalName(adviceElement); - if (BEFORE.equals(elementName)) { - return AspectJMethodBeforeAdvice.class; - } - else if (AFTER.equals(elementName)) { - return AspectJAfterAdvice.class; - } - else if (AFTER_RETURNING_ELEMENT.equals(elementName)) { - return AspectJAfterReturningAdvice.class; - } - else if (AFTER_THROWING_ELEMENT.equals(elementName)) { - return AspectJAfterThrowingAdvice.class; - } - else if (AROUND.equals(elementName)) { - return AspectJAroundAdvice.class; - } - else { - throw new IllegalArgumentException("Unknown advice kind [" + elementName + "]."); - } + return switch (elementName) { + case BEFORE -> AspectJMethodBeforeAdvice.class; + case AFTER -> AspectJAfterAdvice.class; + case AFTER_RETURNING_ELEMENT -> AspectJAfterReturningAdvice.class; + case AFTER_THROWING_ELEMENT -> AspectJAfterThrowingAdvice.class; + case AROUND -> AspectJAroundAdvice.class; + default -> throw new IllegalArgumentException("Unknown advice kind [" + elementName + "]."); + }; } /** diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java index e116ec85947a..d51e472b589c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,10 +42,8 @@ class ScopedProxyBeanDefinitionDecorator implements BeanDefinitionDecorator { @Override public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { boolean proxyTargetClass = true; - if (node instanceof Element ele) { - if (ele.hasAttribute(PROXY_TARGET_CLASS)) { - proxyTargetClass = Boolean.parseBoolean(ele.getAttribute(PROXY_TARGET_CLASS)); - } + if (node instanceof Element ele && ele.hasAttribute(PROXY_TARGET_CLASS)) { + proxyTargetClass = Boolean.parseBoolean(ele.getAttribute(PROXY_TARGET_CLASS)); } // Register the original bean definition as it will be referenced by the scoped proxy diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java index 3a74eca980f9..f3adcd6a6718 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 @@ -23,6 +23,7 @@ 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 @@ -51,6 +52,7 @@ class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { @Override + @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(); 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 41c5b1d21c1d..282d0584be38 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -85,10 +84,7 @@ public class AdvisedSupport extends ProxyConfig implements Advised { private boolean preFiltered = false; /** The AdvisorChainFactory to use. */ - private AdvisorChainFactory advisorChainFactory; - - /** Cache with Method as key and advisor chain List as value. */ - private transient Map> methodCache; + private AdvisorChainFactory advisorChainFactory = DefaultAdvisorChainFactory.INSTANCE; /** * Interfaces to be implemented by the proxy. Held in List to keep the order @@ -102,15 +98,36 @@ public class AdvisedSupport extends ProxyConfig implements Advised { */ private List advisors = new ArrayList<>(); + /** + * List of minimal {@link AdvisorKeyEntry} instances, + * to be assigned to the {@link #advisors} field on reduction. + * @since 6.0.10 + * @see #reduceToAdvisorKey + */ private List advisorKey = this.advisors; + /** Cache with Method as key and advisor chain List as value. */ + @Nullable + private transient Map> methodCache; + + /** Cache with shared interceptors which are not method-specific. */ + @Nullable + private transient volatile List cachedInterceptors; + + /** + * Optional field for {@link AopProxy} implementations to store metadata in. + * Used by {@link JdkDynamicAopProxy}. + * @since 6.1.3 + * @see JdkDynamicAopProxy#JdkDynamicAopProxy(AdvisedSupport) + */ + @Nullable + transient volatile Object proxyMetadataCache; + /** * No-arg constructor for use as a JavaBean. */ public AdvisedSupport() { - this.advisorChainFactory = DefaultAdvisorChainFactory.INSTANCE; - this.methodCache = new ConcurrentHashMap<>(32); } /** @@ -118,19 +135,9 @@ public AdvisedSupport() { * @param interfaces the proxied interfaces */ public AdvisedSupport(Class... interfaces) { - this(); setInterfaces(interfaces); } - /** - * Internal constructor for {@link #getConfigurationOnlyCopy()}. - * @since 6.0.10 - */ - private AdvisedSupport(AdvisorChainFactory advisorChainFactory, Map> methodCache) { - this.advisorChainFactory = advisorChainFactory; - this.methodCache = methodCache; - } - /** * Set the given object as target. @@ -362,8 +369,7 @@ public void addAdvisors(Collection advisors) { private void validateIntroductionAdvisor(IntroductionAdvisor advisor) { advisor.validateInterfaces(); // If the advisor passed validation, we can make the change. - Class[] ifcs = advisor.getInterfaces(); - for (Class ifc : ifcs) { + for (Class ifc : advisor.getInterfaces()) { addInterface(ifc); } } @@ -482,21 +488,45 @@ public int countAdvicesOfType(@Nullable Class adviceClass) { * @return a List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers) */ public List getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class targetClass) { - MethodCacheKey cacheKey = new MethodCacheKey(method); - List cached = this.methodCache.get(cacheKey); - if (cached == null) { - cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( - this, method, targetClass); - this.methodCache.put(cacheKey, cached); + List cachedInterceptors; + if (this.methodCache != null) { + // Method-specific cache for method-specific pointcuts + MethodCacheKey cacheKey = new MethodCacheKey(method); + cachedInterceptors = this.methodCache.get(cacheKey); + if (cachedInterceptors == null) { + cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( + this, method, targetClass); + this.methodCache.put(cacheKey, cachedInterceptors); + } + } + else { + // Shared cache since there are no method-specific advisors (see below). + cachedInterceptors = this.cachedInterceptors; + if (cachedInterceptors == null) { + cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( + this, method, targetClass); + this.cachedInterceptors = cachedInterceptors; + } } - return cached; + return cachedInterceptors; } /** * Invoked when advice has changed. */ protected void adviceChanged() { - this.methodCache.clear(); + this.methodCache = null; + this.cachedInterceptors = null; + this.proxyMetadataCache = null; + + // Initialize method cache if necessary; otherwise, + // cachedInterceptors is going to be shared (see above). + for (Advisor advisor : this.advisors) { + if (advisor instanceof PointcutAdvisor) { + this.methodCache = new ConcurrentHashMap<>(); + break; + } + } } /** @@ -535,21 +565,28 @@ protected void copyConfigurationFrom(AdvisedSupport other, TargetSource targetSo * replacing the {@link TargetSource}. */ AdvisedSupport getConfigurationOnlyCopy() { - AdvisedSupport copy = new AdvisedSupport(this.advisorChainFactory, this.methodCache); + AdvisedSupport copy = new AdvisedSupport(); copy.copyFrom(this); copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic()); + copy.preFiltered = this.preFiltered; + copy.advisorChainFactory = this.advisorChainFactory; copy.interfaces = new ArrayList<>(this.interfaces); copy.advisors = new ArrayList<>(this.advisors); copy.advisorKey = new ArrayList<>(this.advisors.size()); for (Advisor advisor : this.advisors) { copy.advisorKey.add(new AdvisorKeyEntry(advisor)); } + copy.methodCache = this.methodCache; + copy.cachedInterceptors = this.cachedInterceptors; + copy.proxyMetadataCache = this.proxyMetadataCache; return copy; } void reduceToAdvisorKey() { this.advisors = this.advisorKey; - this.methodCache = Collections.emptyMap(); + this.methodCache = null; + this.cachedInterceptors = null; + this.proxyMetadataCache = null; } Object getAdvisorKey() { @@ -557,18 +594,6 @@ Object getAdvisorKey() { } - //--------------------------------------------------------------------- - // Serialization support - //--------------------------------------------------------------------- - - private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { - // Rely on default serialization; just initialize state after deserialization. - ois.defaultReadObject(); - - // Initialize transient fields. - this.methodCache = new ConcurrentHashMap<>(32); - } - @Override public String toProxyConfigString() { return toString(); @@ -590,6 +615,19 @@ public String toString() { } + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize method cache if necessary. + adviceChanged(); + } + + /** * Simple wrapper class around a Method. Used as the key when * caching methods, for efficient equals and hashCode comparisons. @@ -639,7 +677,7 @@ public int compareTo(MethodCacheKey other) { * @see #getConfigurationOnlyCopy() * @see #getAdvisorKey() */ - private static class AdvisorKeyEntry implements Advisor { + private static final class AdvisorKeyEntry implements Advisor { private final Class adviceType; @@ -649,7 +687,6 @@ private static class AdvisorKeyEntry implements Advisor { @Nullable private final String methodMatcherKey; - public AdvisorKeyEntry(Advisor advisor) { this.adviceType = advisor.getAdvice().getClass(); if (advisor instanceof PointcutAdvisor pointcutAdvisor) { 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 3b470e17016e..26651f6200b8 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 @@ -266,7 +266,7 @@ static Object[] adaptArgumentsIfNecessary(Method method, @Nullable Object[] argu if (varargArray instanceof Object[] && !varargType.isInstance(varargArray)) { Object[] newArguments = new Object[arguments.length]; System.arraycopy(arguments, 0, newArguments, 0, varargIndex); - Class targetElementType = varargType.getComponentType(); + Class targetElementType = varargType.componentType(); int varargLength = Array.getLength(varargArray); Object newVarargArray = Array.newInstance(targetElementType, varargLength); System.arraycopy(varargArray, 0, newVarargArray, 0, varargLength); 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 dceed756b414..44bdf08db7ba 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -46,6 +47,7 @@ import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; 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; @@ -74,6 +76,7 @@ * @author Ramnivas Laddad * @author Chris Beams * @author Dave Syer + * @author Sebastien Deleuze * @see org.springframework.cglib.proxy.Enhancer * @see AdvisedSupport#setProxyTargetClass * @see DefaultAopProxyFactory @@ -91,6 +94,11 @@ class CglibAopProxy implements AopProxy, Serializable { private static final int INVOKE_HASHCODE = 6; + private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; + + private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + "kotlinx.coroutines.reactor.MonoKt", CglibAopProxy.class.getClassLoader()); + /** Logger available to subclasses; static to optimize serialization. */ protected static final Log logger = LogFactory.getLog(CglibAopProxy.class); @@ -276,9 +284,9 @@ 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.isInfoEnabled() && implementsInterface(method, ifcs)) { - logger.info("Unable to proxy interface-implementing method [" + method + "] because " + - "it is marked as final: Consider using interface-based JDK proxies instead!"); + 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.isDebugEnabled()) { logger.debug("Final method [" + method + "] cannot get proxied via CGLIB: " + @@ -335,36 +343,39 @@ private Callback[] getCallbacks(Class rootClass) throws Exception { new HashCodeInterceptor(this.advised) }; - Callback[] callbacks; - // If the target is a static one and the advice chain is frozen, // then we can make some optimizations by sending the AOP calls // direct to the target using the fixed chain for that method. if (isStatic && isFrozen) { Method[] methods = rootClass.getMethods(); - Callback[] fixedCallbacks = new Callback[methods.length]; - this.fixedInterceptorMap = CollectionUtils.newHashMap(methods.length); + int methodsCount = methods.length; + List fixedCallbacks = new ArrayList<>(methodsCount); + this.fixedInterceptorMap = CollectionUtils.newHashMap(methodsCount); - // TODO: small memory optimization here (can skip creation for methods with no advice) - for (int x = 0; x < methods.length; x++) { + int advicedMethodCount = methodsCount; + for (int x = 0; x < methodsCount; x++) { Method method = methods[x]; + //do not create advices for non-overridden methods of java.lang.Object + if (method.getDeclaringClass() == Object.class) { + advicedMethodCount--; + continue; + } List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, rootClass); - fixedCallbacks[x] = new FixedChainStaticTargetInterceptor( - chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass()); - this.fixedInterceptorMap.put(method, x); + fixedCallbacks.add(new FixedChainStaticTargetInterceptor( + chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass())); + this.fixedInterceptorMap.put(method, x - (methodsCount - advicedMethodCount) ); } // Now copy both the callbacks from mainCallbacks // and fixedCallbacks into the callbacks array. - callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length]; + Callback[] callbacks = new Callback[mainCallbacks.length + advicedMethodCount]; System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length); - System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length); + System.arraycopy(fixedCallbacks.toArray(Callback[]::new), 0, callbacks, + mainCallbacks.length, advicedMethodCount); this.fixedInterceptorOffset = mainCallbacks.length; + return callbacks; } - else { - callbacks = mainCallbacks; - } - return callbacks; + return mainCallbacks; } @@ -395,10 +406,11 @@ private static boolean implementsInterface(Method method, Set> ifcs) { /** * Process a return value. Wraps a return of {@code this} if necessary to be the * {@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( - Object proxy, @Nullable Object target, Method method, @Nullable Object returnValue) { + Object proxy, @Nullable Object target, Method method, Object[] arguments, @Nullable Object returnValue) { // Massage return value if necessary if (returnValue != null && returnValue == target && @@ -408,10 +420,15 @@ private static Object processReturnType( returnValue = proxy; } Class returnType = method.getReturnType(); - if (returnValue == null && returnType != Void.TYPE && returnType.isPrimitive()) { + if (returnValue == null && returnType != void.class && returnType.isPrimitive()) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } + if (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { + return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ? + CoroutinesUtils.asFlow(returnValue) : + CoroutinesUtils.awaitSingleOrNull(returnValue, arguments[arguments.length - 1]); + } return returnValue; } @@ -442,7 +459,7 @@ public StaticUnadvisedInterceptor(@Nullable Object target) { @Nullable public 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, retVal); + return processReturnType(proxy, this.target, method, args, retVal); } } @@ -467,7 +484,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy try { oldProxy = AopContext.setCurrentProxy(proxy); Object retVal = AopUtils.invokeJoinpointUsingReflection(this.target, method, args); - return processReturnType(proxy, this.target, method, retVal); + return processReturnType(proxy, this.target, method, args, retVal); } finally { AopContext.setCurrentProxy(oldProxy); @@ -495,7 +512,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy Object target = this.targetSource.getTarget(); try { Object retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); - return processReturnType(proxy, target, method, retVal); + return processReturnType(proxy, target, method, args, retVal); } finally { if (target != null) { @@ -525,7 +542,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy try { oldProxy = AopContext.setCurrentProxy(proxy); Object retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); - return processReturnType(proxy, target, method, retVal); + return processReturnType(proxy, target, method, args, retVal); } finally { AopContext.setCurrentProxy(oldProxy); @@ -652,7 +669,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy proxy, this.target, method, args, this.targetClass, this.adviceChain, methodProxy); // If we get here, we need to create a MethodInvocation. Object retVal = invocation.proceed(); - retVal = processReturnType(proxy, this.target, method, retVal); + retVal = processReturnType(proxy, this.target, method, args, retVal); return retVal; } } @@ -702,7 +719,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy // We need to create a method invocation... retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); } - return processReturnType(proxy, target, method, retVal); + return processReturnType(proxy, target, method, args, retVal); } finally { if (target != null && !targetSource.isStatic()) { 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 new file mode 100644 index 000000000000..f1e06096161a --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.framework; + +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.reactive.ReactiveFlowKt; +import kotlinx.coroutines.reactor.MonoKt; +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. + * + * @author Sebastien Deleuze + * @since 6.1 + */ +abstract class CoroutinesUtils { + + static Object asFlow(@Nullable Object publisher) { + if (publisher instanceof Publisher rsPublisher) { + return ReactiveFlowKt.asFlow(rsPublisher); + } + else { + throw new IllegalArgumentException("Not a Reactive Streams Publisher: " + publisher); + } + } + + @Nullable + @SuppressWarnings({"unchecked", "rawtypes"}) + static Object awaitSingleOrNull(@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/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index e5f9c08531dc..3ab70ee9e877 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.aop.framework; +import java.io.IOException; +import java.io.ObjectInputStream; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -31,6 +33,8 @@ import org.springframework.aop.TargetSource; import org.springframework.aop.support.AopUtils; 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; @@ -58,6 +62,7 @@ * @author Rob Harrop * @author Dave Syer * @author Sergey Tsypanov + * @author Sebastien Deleuze * @see java.lang.reflect.Proxy * @see AdvisedSupport * @see ProxyFactory @@ -68,14 +73,10 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa private static final long serialVersionUID = 5531744639992436476L; - /* - * NOTE: We could avoid the code duplication between this class and the CGLIB - * proxies by refactoring "invoke" into a template method. However, this approach - * adds at least 10% performance overhead versus a copy-paste solution, so we sacrifice - * elegance for performance (we have a good test suite to ensure that the different - * proxies behave the same :-)). - * This way, we can also more easily take advantage of minor optimizations in each class. - */ + private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; + + private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + "kotlinx.coroutines.reactor.MonoKt", JdkDynamicAopProxy.class.getClassLoader()); /** We use a static Log to avoid serialization issues. */ private static final Log logger = LogFactory.getLog(JdkDynamicAopProxy.class); @@ -83,17 +84,8 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa /** Config used to configure this proxy. */ private final AdvisedSupport advised; - private final Class[] proxiedInterfaces; - - /** - * Is the {@link #equals} method defined on the proxied interfaces? - */ - private boolean equalsDefined; - - /** - * Is the {@link #hashCode} method defined on the proxied interfaces? - */ - private boolean hashCodeDefined; + /** Cached in {@link AdvisedSupport#proxyMetadataCache}. */ + private transient ProxiedInterfacesCache cache; /** @@ -105,8 +97,17 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { Assert.notNull(config, "AdvisedSupport must not be null"); this.advised = config; - this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); - findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces); + + // Initialize ProxiedInterfacesCache if not cached already + ProxiedInterfacesCache cache; + if (config.proxyMetadataCache instanceof ProxiedInterfacesCache proxiedInterfacesCache) { + cache = proxiedInterfacesCache; + } + else { + cache = new ProxiedInterfacesCache(config); + config.proxyMetadataCache = cache; + } + this.cache = cache; } @@ -120,13 +121,13 @@ public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } - return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this); + return Proxy.newProxyInstance(determineClassLoader(classLoader), this.cache.proxiedInterfaces, this); } @SuppressWarnings("deprecation") @Override public Class getProxyClass(@Nullable ClassLoader classLoader) { - return Proxy.getProxyClass(determineClassLoader(classLoader), this.proxiedInterfaces); + return Proxy.getProxyClass(determineClassLoader(classLoader), this.cache.proxiedInterfaces); } /** @@ -155,28 +156,6 @@ private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) { return classLoader; } - /** - * Finds any {@link #equals} or {@link #hashCode} method that may be defined - * on the supplied set of interfaces. - * @param proxiedInterfaces the interfaces to introspect - */ - private void findDefinedEqualsAndHashCodeMethods(Class[] proxiedInterfaces) { - for (Class proxiedInterface : proxiedInterfaces) { - Method[] methods = proxiedInterface.getDeclaredMethods(); - for (Method method : methods) { - if (AopUtils.isEqualsMethod(method)) { - this.equalsDefined = true; - } - if (AopUtils.isHashCodeMethod(method)) { - this.hashCodeDefined = true; - } - if (this.equalsDefined && this.hashCodeDefined) { - return; - } - } - } - } - /** * Implementation of {@code InvocationHandler.invoke}. @@ -193,11 +172,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Object target = null; try { - if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + if (!this.cache.equalsDefined && AopUtils.isEqualsMethod(method)) { // The target does not implement the equals(Object) method itself. return equals(args[0]); } - else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + else if (!this.cache.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { // The target does not implement the hashCode() method itself. return hashCode(); } @@ -254,10 +233,14 @@ else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && // a reference to itself in another returned object. retVal = proxy; } - else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + else if (retVal == null && returnType != void.class && returnType.isPrimitive()) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } + if (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { + return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ? + CoroutinesUtils.asFlow(retVal) : CoroutinesUtils.awaitSingleOrNull(retVal, args[args.length - 1]); + } return retVal; } finally { @@ -315,4 +298,63 @@ public int hashCode() { return JdkDynamicAopProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode(); } + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.cache = new ProxiedInterfacesCache(this.advised); + } + + + /** + * Holder for the complete proxied interfaces and derived metadata, + * to be cached in {@link AdvisedSupport#proxyMetadataCache}. + * @since 6.1.3 + */ + private static final class ProxiedInterfacesCache { + + final Class[] proxiedInterfaces; + + final boolean equalsDefined; + + final boolean hashCodeDefined; + + ProxiedInterfacesCache(AdvisedSupport config) { + this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(config, true); + + // Find any {@link #equals} or {@link #hashCode} method that may be defined + // on the supplied set of interfaces. + boolean equalsDefined = false; + boolean hashCodeDefined = false; + for (Class proxiedInterface : this.proxiedInterfaces) { + Method[] methods = proxiedInterface.getDeclaredMethods(); + for (Method method : methods) { + if (AopUtils.isEqualsMethod(method)) { + equalsDefined = true; + if (hashCodeDefined) { + break; + } + } + if (AopUtils.isHashCodeMethod(method)) { + hashCodeDefined = true; + if (equalsDefined) { + break; + } + } + } + if (equalsDefined && hashCodeDefined) { + break; + } + } + this.equalsDefined = equalsDefined; + this.hashCodeDefined = hashCodeDefined; + } + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java index fcca4f0d3f2f..2baf3e93b140 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-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.AfterAdvice; +import org.springframework.aop.framework.AopConfigException; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -78,21 +79,44 @@ public ThrowsAdviceInterceptor(Object throwsAdvice) { Method[] methods = throwsAdvice.getClass().getMethods(); for (Method method : methods) { - if (method.getName().equals(AFTER_THROWING) && - (method.getParameterCount() == 1 || method.getParameterCount() == 4)) { - Class throwableParam = method.getParameterTypes()[method.getParameterCount() - 1]; - if (Throwable.class.isAssignableFrom(throwableParam)) { - // An exception handler to register... - this.exceptionHandlerMap.put(throwableParam, method); - if (logger.isDebugEnabled()) { - logger.debug("Found exception handler method on throws advice: " + method); + if (method.getName().equals(AFTER_THROWING)) { + Class throwableParam = null; + if (method.getParameterCount() == 1) { + // just a Throwable parameter + throwableParam = method.getParameterTypes()[0]; + if (!Throwable.class.isAssignableFrom(throwableParam)) { + throw new AopConfigException("Invalid afterThrowing signature: " + + "single argument must be a Throwable subclass"); } } + else if (method.getParameterCount() == 4) { + // Method, Object[], target, throwable + Class[] paramTypes = method.getParameterTypes(); + if (!Method.class.equals(paramTypes[0]) || !Object[].class.equals(paramTypes[1]) || + Throwable.class.equals(paramTypes[2]) || !Throwable.class.isAssignableFrom(paramTypes[3])) { + throw new AopConfigException("Invalid afterThrowing signature: " + + "four arguments must be Method, Object[], target, throwable: " + method); + } + throwableParam = paramTypes[3]; + } + if (throwableParam == null) { + throw new AopConfigException("Unsupported afterThrowing signature: single throwable argument " + + "or four arguments Method, Object[], target, throwable expected: " + method); + } + // An exception handler to register... + Method existingMethod = this.exceptionHandlerMap.put(throwableParam, method); + if (existingMethod != null) { + throw new AopConfigException("Only one afterThrowing method per specific Throwable subclass " + + "allowed: " + method + " / " + existingMethod); + } + if (logger.isDebugEnabled()) { + logger.debug("Found exception handler method on throws advice: " + method); + } } } if (this.exceptionHandlerMap.isEmpty()) { - throw new IllegalArgumentException( + throw new AopConfigException( "At least one handler method must be found in class [" + throwsAdvice.getClass() + "]"); } } 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..7a71cb57569f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ 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; @@ -97,7 +99,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 502fd9e29259..32d4c7e6bdb8 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,7 +138,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport private final Set targetSourcedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); - private final Map earlyProxyReferences = new ConcurrentHashMap<>(16); + private final Map earlyBeanReferences = new ConcurrentHashMap<>(16); private final Map> proxyTypes = new ConcurrentHashMap<>(16); @@ -265,11 +265,12 @@ public Constructor[] determineCandidateConstructors(Class beanClass, Strin @Override public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey = getCacheKey(bean.getClass(), beanName); - this.earlyProxyReferences.put(cacheKey, bean); + this.earlyBeanReferences.put(cacheKey, bean); return wrapIfNecessary(bean, beanName, cacheKey); } @Override + @Nullable public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { Object cacheKey = getCacheKey(beanClass, beanName); @@ -311,10 +312,11 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str * @see #getAdvicesAndAdvisorsForBean */ @Override + @Nullable public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); - if (this.earlyProxyReferences.remove(cacheKey) != bean) { + if (this.earlyBeanReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java index 823961064347..c9ea561366a4 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 @@ -81,6 +81,7 @@ public void setBeanNames(String... beanNames) { * @see #setBeanNames(String...) */ @Override + @Nullable protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { return (isSupportedBeanName(beanClass, beanName) ? super.getCustomTargetSource(beanClass, beanName) : null); diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java index 5d35e87994b5..a9b63d90091e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -58,6 +58,7 @@ * @author Juergen Hoeller * @author Stephane Nicoll * @author He Bo + * @author Sebastien Deleuze * @since 3.1.2 */ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { @@ -73,8 +74,6 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { protected final Log logger = LogFactory.getLog(getClass()); - private final Map executors = new ConcurrentHashMap<>(16); - private SingletonSupplier defaultExecutor; private SingletonSupplier exceptionHandler; @@ -85,6 +84,9 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { @Nullable private StringValueResolver embeddedValueResolver; + private final Map executors = new ConcurrentHashMap<>(16); + + /** * Create a new instance with a default {@link AsyncUncaughtExceptionHandler}. * @param defaultExecutor the {@code Executor} (typically a Spring {@code AsyncTaskExecutor} @@ -157,6 +159,7 @@ public void setBeanFactory(BeanFactory beanFactory) { if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { this.embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); } + this.executors.clear(); } @@ -290,7 +293,7 @@ else if (org.springframework.util.concurrent.ListenableFuture.class.isAssignable else if (Future.class.isAssignableFrom(returnType)) { return executor.submit(task); } - else if (void.class == returnType) { + else if (void.class == returnType || "kotlin.Unit".equals(returnType.getName())) { executor.submit(task); return null; } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java index e89040d1f1e0..c2ea5aad5a4f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,6 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * AOP Alliance {@code MethodInterceptor} that processes method invocations @@ -101,10 +100,9 @@ public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaug @Nullable public Object invoke(final MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); - Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); - final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass); - AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod); + AsyncTaskExecutor executor = determineAsyncExecutor(userMethod); if (executor == null) { throw new IllegalStateException( "No executor specified and no default executor set on AsyncExecutionInterceptor either"); @@ -118,10 +116,10 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { } } catch (ExecutionException ex) { - handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); + handleError(ex.getCause(), userMethod, invocation.getArguments()); } catch (Throwable ex) { - handleError(ex, userDeclaredMethod, invocation.getArguments()); + handleError(ex, userMethod, invocation.getArguments()); } return null; }; 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 bc675836229f..46c879ff1d5c 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-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; -import org.springframework.core.Constants; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -62,6 +61,7 @@ * * @author Rob Harrop * @author Juergen Hoeller + * @author Sam Brannen * @since 1.2 * @see #setEnterMessage * @see #setExitMessage @@ -152,8 +152,15 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor { /** * The {@code Set} of allowed placeholders. */ - private static final Set ALLOWED_PLACEHOLDERS = - new Constants(CustomizableTraceInterceptor.class).getValues("PLACEHOLDER_"); + static final Set ALLOWED_PLACEHOLDERS = Set.of( + PLACEHOLDER_METHOD_NAME, + PLACEHOLDER_TARGET_CLASS_NAME, + PLACEHOLDER_TARGET_CLASS_SHORT_NAME, + PLACEHOLDER_RETURN_VALUE, + PLACEHOLDER_ARGUMENT_TYPES, + PLACEHOLDER_ARGUMENTS, + PLACEHOLDER_EXCEPTION, + PLACEHOLDER_INVOCATION_TIME); /** @@ -295,43 +302,38 @@ protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throw protected String replacePlaceholders(String message, MethodInvocation methodInvocation, @Nullable Object returnValue, @Nullable Throwable throwable, long invocationTime) { - Matcher matcher = PATTERN.matcher(message); Object target = methodInvocation.getThis(); Assert.state(target != null, "Target must not be null"); StringBuilder output = new StringBuilder(); + Matcher matcher = PATTERN.matcher(message); while (matcher.find()) { String match = matcher.group(); - if (PLACEHOLDER_METHOD_NAME.equals(match)) { - matcher.appendReplacement(output, Matcher.quoteReplacement(methodInvocation.getMethod().getName())); - } - else if (PLACEHOLDER_TARGET_CLASS_NAME.equals(match)) { - String className = getClassForLogging(target).getName(); - matcher.appendReplacement(output, Matcher.quoteReplacement(className)); - } - else if (PLACEHOLDER_TARGET_CLASS_SHORT_NAME.equals(match)) { - String shortName = ClassUtils.getShortName(getClassForLogging(target)); - matcher.appendReplacement(output, Matcher.quoteReplacement(shortName)); - } - else if (PLACEHOLDER_ARGUMENTS.equals(match)) { - matcher.appendReplacement(output, + switch (match) { + case PLACEHOLDER_METHOD_NAME -> matcher.appendReplacement(output, + Matcher.quoteReplacement(methodInvocation.getMethod().getName())); + case PLACEHOLDER_TARGET_CLASS_NAME -> { + String className = getClassForLogging(target).getName(); + matcher.appendReplacement(output, Matcher.quoteReplacement(className)); + } + case PLACEHOLDER_TARGET_CLASS_SHORT_NAME -> { + String shortName = ClassUtils.getShortName(getClassForLogging(target)); + matcher.appendReplacement(output, Matcher.quoteReplacement(shortName)); + } + case PLACEHOLDER_ARGUMENTS -> matcher.appendReplacement(output, Matcher.quoteReplacement(StringUtils.arrayToCommaDelimitedString(methodInvocation.getArguments()))); - } - else if (PLACEHOLDER_ARGUMENT_TYPES.equals(match)) { - appendArgumentTypes(methodInvocation, matcher, output); - } - else if (PLACEHOLDER_RETURN_VALUE.equals(match)) { - appendReturnValue(methodInvocation, matcher, output, returnValue); - } - else if (throwable != null && PLACEHOLDER_EXCEPTION.equals(match)) { - matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString())); - } - else if (PLACEHOLDER_INVOCATION_TIME.equals(match)) { - matcher.appendReplacement(output, Long.toString(invocationTime)); - } - else { - // Should not happen since placeholders are checked earlier. - throw new IllegalArgumentException("Unknown placeholder [" + match + "]"); + case PLACEHOLDER_ARGUMENT_TYPES -> appendArgumentTypes(methodInvocation, matcher, output); + case PLACEHOLDER_RETURN_VALUE -> appendReturnValue(methodInvocation, matcher, output, returnValue); + case PLACEHOLDER_EXCEPTION -> { + if (throwable != null) { + matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString())); + } + } + case PLACEHOLDER_INVOCATION_TIME -> matcher.appendReplacement(output, Long.toString(invocationTime)); + default -> { + // Should not happen since placeholders are checked earlier. + throw new IllegalArgumentException("Unknown placeholder [" + match + "]"); + } } } matcher.appendTail(output); @@ -348,7 +350,7 @@ else if (PLACEHOLDER_INVOCATION_TIME.equals(match)) { * @param output the {@code StringBuilder} to write output to * @param returnValue the value returned by the method invocation. */ - private void appendReturnValue( + private static void appendReturnValue( MethodInvocation methodInvocation, Matcher matcher, StringBuilder output, @Nullable Object returnValue) { if (methodInvocation.getMethod().getReturnType() == void.class) { @@ -372,7 +374,7 @@ else if (returnValue == null) { * @param matcher the {@code Matcher} containing the state of the output * @param output the {@code StringBuilder} containing the output */ - private void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) { + private static void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) { Class[] argumentTypes = methodInvocation.getMethod().getParameterTypes(); String[] argumentTypeShortNames = new String[argumentTypes.length]; for (int i = 0; i < argumentTypeShortNames.length; i++) { @@ -387,7 +389,7 @@ private void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matc * that are not specified as constants on this class and throws an * {@code IllegalArgumentException} if so. */ - private void checkForInvalidPlaceholders(String message) throws IllegalArgumentException { + private static void checkForInvalidPlaceholders(String message) throws IllegalArgumentException { Matcher matcher = PATTERN.matcher(message); while (matcher.find()) { String match = matcher.group(); 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 dbf2df4fa192..830ca6e7f5b8 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.aop.scope; -import java.lang.reflect.Executable; import java.util.function.Predicate; import javax.lang.model.element.Modifier; @@ -54,6 +53,7 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc @Override + @Nullable public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (beanClass.equals(ScopedProxyFactoryBean.class)) { @@ -79,8 +79,8 @@ private String getTargetBeanName(BeanDefinition beanDefinition) { } @Nullable - private BeanDefinition getTargetBeanDefinition(ConfigurableBeanFactory beanFactory, - @Nullable String targetBeanName) { + private BeanDefinition getTargetBeanDefinition( + ConfigurableBeanFactory beanFactory, @Nullable String targetBeanName) { if (targetBeanName != null && beanFactory.containsBean(targetBeanName)) { return beanFactory.getMergedBeanDefinition(targetBeanName); @@ -109,7 +109,7 @@ private static class ScopedProxyBeanRegistrationCodeFragments extends BeanRegist } @Override - public ClassName getTarget(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { + public ClassName getTarget(RegisteredBean registeredBean) { return ClassName.get(this.targetBeanDefinition.getResolvableType().toClass()); } @@ -123,42 +123,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, - Executable constructorOrFactoryMethod, + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { GeneratedMethod generatedMethod = beanRegistrationCode.getMethods() .add("getScopedProxyInstance", method -> { - method.addJavadoc( - "Create the scoped proxy bean instance for '$L'.", + method.addJavadoc("Create the scoped proxy bean instance for '$L'.", this.registeredBean.getBeanName()); method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); method.returns(ScopedProxyFactoryBean.class); - method.addParameter(RegisteredBean.class, - REGISTERED_BEAN_PARAMETER_NAME); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER_NAME); method.addStatement("$T factory = new $T()", - ScopedProxyFactoryBean.class, - ScopedProxyFactoryBean.class); - method.addStatement("factory.setTargetBeanName($S)", - this.targetBeanName); - method.addStatement( - "factory.setBeanFactory($L.getBeanFactory())", + ScopedProxyFactoryBean.class, ScopedProxyFactoryBean.class); + method.addStatement("factory.setTargetBeanName($S)", this.targetBeanName); + method.addStatement("factory.setBeanFactory($L.getBeanFactory())", REGISTERED_BEAN_PARAMETER_NAME); method.addStatement("return factory"); }); diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java index fe7934b0ca0c..a787a1ee809c 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 @@ -117,6 +117,7 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override + @Nullable public Object getObject() { if (this.proxy == null) { throw new FactoryBeanNotInitializedException(); @@ -125,6 +126,7 @@ public Object getObject() { } @Override + @Nullable public Class getObjectType() { if (this.proxy != null) { return this.proxy.getClass(); 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 dcc8670f8706..be3af49edb6a 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,10 @@ import java.util.List; import java.util.Set; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.CoroutineContext; +import kotlinx.coroutines.Job; + import org.springframework.aop.Advisor; import org.springframework.aop.AopInvocationException; import org.springframework.aop.IntroductionAdvisor; @@ -35,6 +39,8 @@ import org.springframework.aop.SpringProxy; import org.springframework.aop.TargetClassAware; import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.CoroutinesUtils; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -53,10 +59,15 @@ * @author Rod Johnson * @author Juergen Hoeller * @author Rob Harrop + * @author Sebastien Deleuze * @see org.springframework.aop.framework.AopProxyUtils */ public abstract class AopUtils { + private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + "kotlinx.coroutines.reactor.MonoKt", AopUtils.class.getClassLoader()); + + /** * Check whether the given object is a JDK dynamic proxy or a CGLIB proxy. *

This method additionally checks if the given object is an instance @@ -186,17 +197,16 @@ public static boolean isFinalizeMethod(@Nullable Method method) { * this method resolves bridge methods in order to retrieve attributes from * the original method definition. * @param method the method to be invoked, which may come from an interface - * @param targetClass the target class for the current invocation. - * May be {@code null} or may not even implement the method. + * @param targetClass the target class for the current invocation + * (can be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the - * {@code targetClass} doesn't implement it or is {@code null} + * {@code targetClass} does not implement it * @see org.springframework.util.ClassUtils#getMostSpecificMethod + * @see org.springframework.core.BridgeMethodResolver#getMostSpecificMethod */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { Class specificTargetClass = (targetClass != null ? ClassUtils.getUserClass(targetClass) : null); - Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass); - // If we are dealing with method with generic parameters, find the original method. - return BridgeMethodResolver.findBridgedMethod(resolvedMethod); + return BridgeMethodResolver.getMostSpecificMethod(method, specificTargetClass); } /** @@ -339,8 +349,10 @@ public static Object invokeJoinpointUsingReflection(@Nullable Object target, Met // Use reflection to invoke the method. try { - ReflectionUtils.makeAccessible(method); - return method.invoke(target, args); + Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); + ReflectionUtils.makeAccessible(originalMethod); + return (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(originalMethod) ? + KotlinDelegate.invokeSuspendingFunction(originalMethod, target, args) : originalMethod.invoke(target, args)); } catch (InvocationTargetException ex) { // Invoked method threw a checked exception. @@ -356,4 +368,18 @@ public static Object invokeJoinpointUsingReflection(@Nullable Object target, Met } } + + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + public static Object invokeSuspendingFunction(Method method, @Nullable Object target, Object... args) { + Continuation continuation = (Continuation) args[args.length -1]; + Assert.state(continuation != null, "No Continuation available"); + CoroutineContext context = continuation.getContext().minusKey(Job.Key); + return CoroutinesUtils.invokeSuspendingFunction(context, method, target, args); + } + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java index d3d6d7463f90..929196e66b74 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 @@ -18,6 +18,7 @@ import java.io.Serializable; import java.util.Arrays; +import java.util.Objects; import org.springframework.aop.ClassFilter; import org.springframework.lang.Nullable; @@ -85,6 +86,18 @@ public static ClassFilter intersection(ClassFilter[] classFilters) { return new IntersectionClassFilter(classFilters); } + /** + * Return a class filter that represents the logical negation of the specified + * filter instance. + * @param classFilter the {@link ClassFilter} to negate + * @return a filter that represents the logical negation of the specified filter + * @since 6.1 + */ + public static ClassFilter negate(ClassFilter classFilter) { + Assert.notNull(classFilter, "ClassFilter must not be null"); + return new NegateClassFilter(classFilter); + } + /** * ClassFilter implementation for a union of the given ClassFilters. @@ -116,7 +129,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.filters); + return Arrays.hashCode(this.filters); } @Override @@ -156,7 +169,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.filters); + return Arrays.hashCode(this.filters); } @Override @@ -165,4 +178,39 @@ public String toString() { } } + + /** + * ClassFilter implementation for a logical negation of the given ClassFilter. + */ + @SuppressWarnings("serial") + private static class NegateClassFilter implements ClassFilter, Serializable { + + private final ClassFilter original; + + NegateClassFilter(ClassFilter original) { + this.original = original; + } + + @Override + public boolean matches(Class clazz) { + return !this.original.matches(clazz); + } + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof NegateClassFilter that + && this.original.equals(that.original))); + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), this.original); + } + + @Override + public String toString() { + return "Negate " + this.original; + } + } + } 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 67eeecea0e00..43707df5c200 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,9 @@ import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.aop.ClassFilter; @@ -25,53 +28,100 @@ import org.springframework.aop.Pointcut; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; +import org.springframework.util.PatternMatchUtils; /** - * Pointcut and method matcher for use in simple cflow-style pointcut. - * Note that evaluating such pointcuts is 10-15 times slower than evaluating + * Pointcut and method matcher for use as a simple cflow-style pointcut. + * + *

Each configured method name pattern can be an exact method name or a + * pattern (see {@link #isMatch(String, String)} for details on the supported + * pattern styles). + * + *

Note that evaluating such pointcuts is 10-15 times slower than evaluating * normal pointcuts, but they are useful in some cases. * * @author Rod Johnson * @author Rob Harrop * @author Juergen Hoeller * @author Sam Brannen + * @see NameMatchMethodPointcut + * @see JdkRegexpMethodPointcut */ @SuppressWarnings("serial") public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher, Serializable { - private final Class clazz; + /** + * The class against which to match. + * @since 6.1 + */ + protected final Class clazz; - @Nullable - private final String methodName; + /** + * An immutable list of distinct method name patterns against which to match. + * @since 6.1 + */ + protected final List methodNamePatterns; - private final AtomicInteger evaluations = new AtomicInteger(); + private final AtomicInteger evaluationCount = new AtomicInteger(); /** - * Construct a new pointcut that matches all control flows below that class. - * @param clazz the clazz + * Construct a new pointcut that matches all control flows below the given class. + * @param clazz the class */ public ControlFlowPointcut(Class clazz) { - this(clazz, null); + this(clazz, (String) null); } /** - * Construct a new pointcut that matches all calls below the given method - * in the given class. If no method name is given, matches all control flows + * Construct a new pointcut that matches all calls below a method matching + * the given method name pattern in the given class. + *

If no method name pattern is given, the pointcut matches all control flows * below the given class. - * @param clazz the clazz - * @param methodName the name of the method (may be {@code null}) + * @param clazz the class + * @param methodNamePattern the method name pattern (may be {@code null}) */ - public ControlFlowPointcut(Class clazz, @Nullable String methodName) { + public ControlFlowPointcut(Class clazz, @Nullable String methodNamePattern) { Assert.notNull(clazz, "Class must not be null"); this.clazz = clazz; - this.methodName = methodName; + this.methodNamePatterns = (methodNamePattern != null ? + Collections.singletonList(methodNamePattern) : Collections.emptyList()); + } + + /** + * Construct a new pointcut that matches all calls below a method matching + * one of the given method name patterns in the given class. + *

If no method name pattern is given, the pointcut matches all control flows + * below the given class. + * @param clazz the class + * @param methodNamePatterns the method name patterns (potentially empty) + * @since 6.1 + */ + public ControlFlowPointcut(Class clazz, String... methodNamePatterns) { + this(clazz, Arrays.asList(methodNamePatterns)); + } + + /** + * Construct a new pointcut that matches all calls below a method matching + * one of the given method name patterns in the given class. + *

If no method name pattern is given, the pointcut matches all control flows + * below the given class. + * @param clazz the class + * @param methodNamePatterns the method name patterns (potentially empty) + * @since 6.1 + */ + public ControlFlowPointcut(Class clazz, List methodNamePatterns) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodNamePatterns, "List of method name patterns must not be null"); + Assert.noNullElements(methodNamePatterns, "List of method name patterns must not contain null elements"); + this.clazz = clazz; + this.methodNamePatterns = methodNamePatterns.stream().distinct().toList(); } /** * Subclasses can override this for greater filtering (and performance). + *

The default implementation always returns {@code true}. */ @Override public boolean matches(Class clazz) { @@ -80,6 +130,7 @@ public boolean matches(Class clazz) { /** * Subclasses can override this if it's possible to filter out some candidate classes. + *

The default implementation always returns {@code true}. */ @Override public boolean matches(Method method, Class targetClass) { @@ -93,22 +144,81 @@ public boolean isRuntime() { @Override public boolean matches(Method method, Class targetClass, Object... args) { - this.evaluations.incrementAndGet(); + incrementEvaluationCount(); for (StackTraceElement element : new Throwable().getStackTrace()) { - if (element.getClassName().equals(this.clazz.getName()) && - (this.methodName == null || element.getMethodName().equals(this.methodName))) { - return true; + if (element.getClassName().equals(this.clazz.getName())) { + if (this.methodNamePatterns.isEmpty()) { + return true; + } + String methodName = element.getMethodName(); + for (int i = 0; i < this.methodNamePatterns.size(); i++) { + if (isMatch(methodName, i)) { + return true; + } + } } } return false; } /** - * It's useful to know how many times we've fired, for optimization. + * Get the number of times {@link #matches(Method, Class, Object...)} has been + * evaluated. + *

Useful for optimization and testing purposes. */ public int getEvaluations() { - return this.evaluations.get(); + return this.evaluationCount.get(); + } + + /** + * Increment the {@link #getEvaluations() evaluation count}. + * @since 6.1 + * @see #matches(Method, Class, Object...) + */ + protected final void incrementEvaluationCount() { + this.evaluationCount.incrementAndGet(); + } + + /** + * Determine if the given method name matches the method name pattern at the + * specified index. + *

This method is invoked by {@link #matches(Method, Class, Object...)}. + *

The default implementation retrieves the method name pattern from + * {@link #methodNamePatterns} and delegates to {@link #isMatch(String, String)}. + *

Can be overridden in subclasses — for example, to support + * regular expressions. + * @param methodName the method name to check + * @param patternIndex the index of the method name pattern + * @return {@code true} if the method name matches the pattern at the specified + * index + * @since 6.1 + * @see #methodNamePatterns + * @see #isMatch(String, String) + * @see #matches(Method, Class, Object...) + */ + protected boolean isMatch(String methodName, int patternIndex) { + String methodNamePattern = this.methodNamePatterns.get(patternIndex); + return isMatch(methodName, methodNamePattern); + } + + /** + * Determine if the given method name matches the method name pattern. + *

This method is invoked by {@link #isMatch(String, int)}. + *

The default implementation checks for direct equality as well as + * {@code xxx*}, {@code *xxx}, {@code *xxx*}, and {@code xxx*yyy} matches. + *

Can be overridden in subclasses — for example, to support a + * different style of simple pattern matching. + * @param methodName the method name to check + * @param methodNamePattern the method name pattern + * @return {@code true} if the method name matches the pattern + * @since 6.1 + * @see #isMatch(String, int) + * @see PatternMatchUtils#simpleMatch(String, String) + */ + protected boolean isMatch(String methodName, String methodNamePattern) { + return (methodName.equals(methodNamePattern) || + PatternMatchUtils.simpleMatch(methodNamePattern, methodName)); } @@ -126,22 +236,19 @@ public MethodMatcher getMethodMatcher() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof ControlFlowPointcut that && - this.clazz.equals(that.clazz)) && - ObjectUtils.nullSafeEquals(this.methodName, that.methodName)); + this.clazz.equals(that.clazz)) && this.methodNamePatterns.equals(that.methodNamePatterns)); } @Override public int hashCode() { int code = this.clazz.hashCode(); - if (this.methodName != null) { - code = 37 * code + this.methodName.hashCode(); - } + code = 37 * code + this.methodNamePatterns.hashCode(); return code; } @Override public String toString() { - return getClass().getName() + ": class = " + this.clazz.getName() + "; methodName = " + this.methodName; + return getClass().getName() + ": class = " + this.clazz.getName() + "; methodNamePatterns = " + this.methodNamePatterns; } } 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 22e8b44d2dbc..f2d226adfb28 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 @@ -18,6 +18,7 @@ import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Objects; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; @@ -81,6 +82,18 @@ public static MethodMatcher intersection(MethodMatcher mm1, MethodMatcher mm2) { new IntersectionIntroductionAwareMethodMatcher(mm1, mm2) : new IntersectionMethodMatcher(mm1, mm2)); } + /** + * Return a method matcher that represents the logical negation of the specified + * matcher instance. + * @param methodMatcher the {@link MethodMatcher} to negate + * @return a matcher that represents the logical negation of the specified matcher + * @since 6.1 + */ + public static MethodMatcher negate(MethodMatcher methodMatcher) { + Assert.notNull(methodMatcher, "MethodMatcher must not be null"); + return new NegateMethodMatcher(methodMatcher); + } + /** * Apply the given MethodMatcher to the given Method, supporting an * {@link org.springframework.aop.IntroductionAwareMethodMatcher} @@ -338,4 +351,47 @@ public boolean matches(Method method, Class targetClass, boolean hasIntroduct } } + + @SuppressWarnings("serial") + private static class NegateMethodMatcher implements MethodMatcher, Serializable { + + private final MethodMatcher original; + + NegateMethodMatcher(MethodMatcher original) { + this.original = original; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return !this.original.matches(method, targetClass); + } + + @Override + public boolean isRuntime() { + return this.original.isRuntime(); + } + + @Override + public boolean matches(Method method, Class targetClass, 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))); + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), this.original); + } + + @Override + public String toString() { + return "Negate " + this.original; + } + + } + } 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 753a16d7db51..9a11e60b8733 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 @@ -26,59 +26,68 @@ import org.springframework.util.PatternMatchUtils; /** - * Pointcut bean for simple method name matches, as an alternative to regexp patterns. + * Pointcut bean for simple method name matches, as an alternative to regular + * expression patterns. + * + *

Each configured method name can be an exact method name or a method name + * pattern (see {@link #isMatch(String, String)} for details on the supported + * pattern styles). * *

Does not handle overloaded methods: all methods with a given name will be eligible. * * @author Juergen Hoeller * @author Rod Johnson * @author Rob Harrop + * @author Sam Brannen * @since 11.02.2004 * @see #isMatch + * @see JdkRegexpMethodPointcut */ @SuppressWarnings("serial") public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable { - private List mappedNames = new ArrayList<>(); + private List mappedNamePatterns = new ArrayList<>(); /** - * Convenience method when we have only a single method name to match. - * Use either this method or {@code setMappedNames}, not both. + * Convenience method for configuring a single method name pattern. + *

Use either this method or {@link #setMappedNames(String...)}, but not both. * @see #setMappedNames */ - public void setMappedName(String mappedName) { - setMappedNames(mappedName); + public void setMappedName(String mappedNamePattern) { + setMappedNames(mappedNamePattern); } /** - * Set the method names defining methods to match. - * Matching will be the union of all these; if any match, - * the pointcut matches. + * Set the method name patterns defining methods to match. + *

Matching will be the union of all these; if any match, the pointcut matches. + * @see #setMappedName(String) */ - public void setMappedNames(String... mappedNames) { - this.mappedNames = new ArrayList<>(Arrays.asList(mappedNames)); + public void setMappedNames(String... mappedNamePatterns) { + this.mappedNamePatterns = new ArrayList<>(Arrays.asList(mappedNamePatterns)); } /** - * Add another eligible method name, in addition to those already named. - * Like the set methods, this method is for use when configuring proxies, + * Add another method name pattern, in addition to those already configured. + *

Like the "set" methods, this method is for use when configuring proxies, * before a proxy is used. - *

NB: This method does not work after the proxy is in - * use, as advice chains will be cached. - * @param name the name of the additional method that will match - * @return this pointcut to allow for multiple additions in one line + *

NOTE: This method does not work after the proxy is in use, since + * advice chains will be cached. + * @param mappedNamePattern the additional method name pattern + * @return this pointcut to allow for method chaining + * @see #setMappedNames(String...) + * @see #setMappedName(String) */ - public NameMatchMethodPointcut addMethodName(String name) { - this.mappedNames.add(name); + public NameMatchMethodPointcut addMethodName(String mappedNamePattern) { + this.mappedNamePatterns.add(mappedNamePattern); return this; } @Override public boolean matches(Method method, Class targetClass) { - for (String mappedName : this.mappedNames) { - if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) { + for (String mappedNamePattern : this.mappedNamePatterns) { + if (mappedNamePattern.equals(method.getName()) || isMatch(method.getName(), mappedNamePattern)) { return true; } } @@ -86,33 +95,34 @@ public boolean matches(Method method, Class targetClass) { } /** - * Return if the given method name matches the mapped name. - *

The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, - * as well as direct equality. Can be overridden in subclasses. - * @param methodName the method name of the class - * @param mappedName the name in the descriptor - * @return if the names match - * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + * Determine if the given method name matches the mapped name pattern. + *

The default implementation checks for {@code xxx*}, {@code *xxx}, + * {@code *xxx*}, and {@code xxx*yyy} matches, as well as direct equality. + *

Can be overridden in subclasses. + * @param methodName the method name to check + * @param mappedNamePattern the method name pattern + * @return {@code true} if the method name matches the pattern + * @see PatternMatchUtils#simpleMatch(String, String) */ - protected boolean isMatch(String methodName, String mappedName) { - return PatternMatchUtils.simpleMatch(mappedName, methodName); + protected boolean isMatch(String methodName, String mappedNamePattern) { + return PatternMatchUtils.simpleMatch(mappedNamePattern, methodName); } @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof NameMatchMethodPointcut that && - this.mappedNames.equals(that.mappedNames))); + this.mappedNamePatterns.equals(that.mappedNamePatterns))); } @Override public int hashCode() { - return this.mappedNames.hashCode(); + return this.mappedNamePatterns.hashCode(); } @Override public String toString() { - return getClass().getName() + ": " + this.mappedNames; + return getClass().getName() + ": " + this.mappedNamePatterns; } } 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 7e2ac454dfd5..35f3f644f54c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,7 +101,7 @@ private static class SetterPointcut extends StaticMethodMatcherPointcut implemen public boolean matches(Method method, Class targetClass) { return (method.getName().startsWith("set") && method.getParameterCount() == 1 && - method.getReturnType() == Void.TYPE); + method.getReturnType() == void.class); } private Object readResolve() { @@ -126,7 +126,8 @@ private static class GetterPointcut extends StaticMethodMatcherPointcut implemen @Override public boolean matches(Method method, Class targetClass) { return (method.getName().startsWith("get") && - method.getParameterCount() == 0); + method.getParameterCount() == 0 && + method.getReturnType() != void.class); } private Object readResolve() { 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 4c789587955f..6ab195c0a7bd 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 @@ -17,6 +17,7 @@ package org.springframework.aop.target; import java.io.Serializable; +import java.util.Objects; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -152,16 +153,6 @@ public Class getTargetClass() { } } - @Override - public boolean isStatic() { - return false; - } - - @Override - public void releaseTarget(Object target) throws Exception { - // Nothing to do here. - } - /** * Copy configuration from the other AbstractBeanFactoryBasedTargetSource object. @@ -190,7 +181,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return getClass().hashCode() * 13 + ObjectUtils.nullSafeHashCode(this.targetBeanName); + return Objects.hash(getClass(), this.targetBeanName); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java index 8246e1e6268f..57838757015d 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 @@ -72,11 +72,6 @@ public synchronized Class getTargetClass() { return (this.lazyTarget != null ? this.lazyTarget.getClass() : null); } - @Override - public boolean isStatic() { - return false; - } - /** * Returns the lazy-initialized target object, * creating it on-the-fly if it doesn't exist already. @@ -91,11 +86,6 @@ public synchronized Object getTarget() throws Exception { return this.lazyTarget; } - @Override - public void releaseTarget(Object target) throws Exception { - // nothing to do - } - /** * Subclasses should implement this method to return the lazy initialized object. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java index 1486cff2a9a8..cfcb3b119fdf 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 @@ -17,6 +17,7 @@ package org.springframework.aop.target; import java.io.Serializable; +import java.util.Objects; import org.springframework.aop.TargetSource; import org.springframework.lang.Nullable; @@ -115,13 +116,6 @@ public Object getTarget() { return null; } - /** - * Nothing to release. - */ - @Override - public void releaseTarget(Object target) { - } - /** * Returns the canonical instance on deserialization in case @@ -140,7 +134,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return EmptyTargetSource.class.hashCode() * 13 + ObjectUtils.nullSafeHashCode(this.targetClass); + return Objects.hash(getClass(), this.targetClass); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java index 1a1a2fa7552a..fb5aceefcadf 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 @@ -66,21 +66,11 @@ public synchronized Class getTargetClass() { return this.target.getClass(); } - @Override - public final boolean isStatic() { - return false; - } - @Override public synchronized Object getTarget() { return this.target; } - @Override - public void releaseTarget(Object target) { - // nothing to do - } - /** * Swap the target, returning the old target object. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java index e1b2ae6a56c7..11e3a9e8fead 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 @@ -67,11 +67,6 @@ public Object getTarget() { return this.target; } - @Override - public void releaseTarget(Object target) { - // nothing to do - } - @Override public boolean isStatic() { return true; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java index 32c6ce1c928f..e6f386bf55e8 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,14 +73,6 @@ public synchronized Class getTargetClass() { return this.targetObject.getClass(); } - /** - * Not static. - */ - @Override - public boolean isStatic() { - return false; - } - @Override @Nullable public final synchronized Object getTarget() { @@ -90,13 +82,6 @@ public final synchronized Object getTarget() { return this.targetObject; } - /** - * No need to release target. - */ - @Override - public void releaseTarget(Object object) { - } - @Override public final synchronized void refresh() { diff --git a/spring-aop/src/main/resources/META-INF/spring/aot.factories b/spring-aop/src/main/resources/META-INF/spring/aot.factories index e3e7529ad300..c44e9d5d589f 100644 --- a/spring-aop/src/main/resources/META-INF/spring/aot.factories +++ b/spring-aop/src/main/resources/META-INF/spring/aot.factories @@ -1,5 +1,6 @@ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ -org.springframework.aop.scope.ScopedProxyBeanRegistrationAotProcessor +org.springframework.aop.scope.ScopedProxyBeanRegistrationAotProcessor,\ +org.springframework.aop.aspectj.annotation.AspectJAdvisorBeanRegistrationAotProcessor org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor= \ org.springframework.aop.aspectj.annotation.AspectJBeanFactoryInitializationAotProcessor diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java index 16f7b6b7d83e..d361600e6ea5 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Unit tests for {@link AspectJAdviceParameterNameDiscoverer}. + * Tests for {@link AspectJAdviceParameterNameDiscoverer}. * * @author Adrian Colyer * @author Chris Beams diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index e1b2a93b9589..0e93dafdf462 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.aspectj.weaver.tools.PointcutExpression; -import org.aspectj.weaver.tools.PointcutPrimitive; -import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import test.annotation.EmptySpringAnnotation; @@ -42,18 +39,16 @@ import org.springframework.beans.testfixture.beans.subpkg.DeepBean; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * @author Rob Harrop * @author Rod Johnson * @author Chris Beams + * @author Juergen Hoeller + * @author Yanming Zhou */ -public class AspectJExpressionPointcutTests { - - public static final String MATCH_ALL_METHODS = "execution(* *(..))"; +class AspectJExpressionPointcutTests { private Method getAge; @@ -65,7 +60,7 @@ public class AspectJExpressionPointcutTests { @BeforeEach - public void setUp() throws NoSuchMethodException { + void setup() throws NoSuchMethodException { getAge = TestBean.class.getMethod("getAge"); setAge = TestBean.class.getMethod("setAge", int.class); setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class); @@ -78,7 +73,7 @@ public void setUp() throws NoSuchMethodException { @Test - public void testMatchExplicit() { + void testMatchExplicit() { String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; Pointcut pointcut = getPointcut(expression); @@ -96,7 +91,7 @@ public void testMatchExplicit() { } @Test - public void testMatchWithTypePattern() throws Exception { + void testMatchWithTypePattern() { String expression = "execution(* *..TestBean.*Age(..))"; Pointcut pointcut = getPointcut(expression); @@ -115,12 +110,12 @@ public void testMatchWithTypePattern() throws Exception { @Test - public void testThis() throws SecurityException, NoSuchMethodException{ + void testThis() throws SecurityException, NoSuchMethodException{ testThisOrTarget("this"); } @Test - public void testTarget() throws SecurityException, NoSuchMethodException { + void testTarget() throws SecurityException, NoSuchMethodException { testThisOrTarget("target"); } @@ -144,12 +139,12 @@ private void testThisOrTarget(String which) throws SecurityException, NoSuchMeth } @Test - public void testWithinRootPackage() throws SecurityException, NoSuchMethodException { + void testWithinRootPackage() throws SecurityException, NoSuchMethodException { testWithinPackage(false); } @Test - public void testWithinRootAndSubpackages() throws SecurityException, NoSuchMethodException { + void testWithinRootAndSubpackages() throws SecurityException, NoSuchMethodException { testWithinPackage(true); } @@ -173,32 +168,32 @@ private void testWithinPackage(boolean matchSubpackages) throws SecurityExceptio } @Test - public void testFriendlyErrorOnNoLocationClassMatching() { + void testFriendlyErrorOnNoLocationClassMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getClassFilter().matches(ITestBean.class)) + .withMessageContaining("expression"); } @Test - public void testFriendlyErrorOnNoLocation2ArgMatching() { + void testFriendlyErrorOnNoLocation2ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class)) + .withMessageContaining("expression"); } @Test - public void testFriendlyErrorOnNoLocation3ArgMatching() { + void testFriendlyErrorOnNoLocation3ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class, (Object[]) null)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class, (Object[]) null)) + .withMessageContaining("expression"); } @Test - public void testMatchWithArgs() throws Exception { + void testMatchWithArgs() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; Pointcut pointcut = getPointcut(expression); @@ -210,14 +205,16 @@ public void testMatchWithArgs() throws Exception { // not currently testable in a reliable fashion //assertDoesNotMatchStringClass(classFilter); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)).as("Should match with setSomeNumber with Double input").isTrue(); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)).as("Should not match setSomeNumber with Integer input").isFalse(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)) + .as("Should match with setSomeNumber with Double input").isTrue(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)) + .as("Should not match setSomeNumber with Integer input").isFalse(); assertThat(methodMatcher.matches(getAge, TestBean.class)).as("Should not match getAge").isFalse(); assertThat(methodMatcher.isRuntime()).as("Should be a runtime match").isTrue(); } @Test - public void testSimpleAdvice() { + void testSimpleAdvice() { String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; CallCountingInterceptor interceptor = new CallCountingInterceptor(); TestBean testBean = getAdvisedProxy(expression, interceptor); @@ -230,7 +227,7 @@ public void testSimpleAdvice() { } @Test - public void testDynamicMatchingProxy() { + void testDynamicMatchingProxy() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; CallCountingInterceptor interceptor = new CallCountingInterceptor(); TestBean testBean = getAdvisedProxy(expression, interceptor); @@ -244,16 +241,15 @@ public void testDynamicMatchingProxy() { } @Test - public void testInvalidExpression() { + void testInvalidExpression() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; - assertThatIllegalArgumentException().isThrownBy( - getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) { TestBean target = new TestBean(); - Pointcut pointcut = getPointcut(pointcutExpression); + AspectJExpressionPointcut pointcut = getPointcut(pointcutExpression); DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(interceptor); @@ -275,44 +271,33 @@ private void assertMatchesTestBeanClass(ClassFilter classFilter) { } @Test - public void testWithUnsupportedPointcutPrimitive() { + void testWithUnsupportedPointcutPrimitive() { String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; - assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class).isThrownBy(() -> - getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution... - .satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL)); + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } @Test - public void testAndSubstitution() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String)"); + void testAndSubstitution() { + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String)"); } @Test - public void testMultipleAndSubstitutions() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); + void testMultipleAndSubstitutions() { + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); } - private Pointcut getPointcut(String expression) { + private AspectJExpressionPointcut getPointcut(String expression) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(expression); return pointcut; } - - public static class OtherIOther implements IOther { - - @Override - public void absquatulate() { - // Empty - } - } - @Test - public void testMatchGenericArgument() { + void testMatchGenericArgument() { String expression = "execution(* set*(java.util.List) )"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -331,7 +316,7 @@ public void testMatchGenericArgument() { } @Test - public void testMatchVarargs() throws Exception { + void testMatchVarargs() throws Exception { @SuppressWarnings("unused") class MyTemplate { @@ -357,19 +342,19 @@ public int queryForInt(String sql, Object... params) { } @Test - public void testMatchAnnotationOnClassWithAtWithin() throws Exception { + void testMatchAnnotationOnClassWithAtWithin() throws Exception { String expression = "@within(test.annotation.transaction.Tx)"; testMatchAnnotationOnClass(expression); } @Test - public void testMatchAnnotationOnClassWithoutBinding() throws Exception { + void testMatchAnnotationOnClassWithoutBinding() throws Exception { String expression = "within(@test.annotation.transaction.Tx *)"; testMatchAnnotationOnClass(expression); } @Test - public void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { + void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { String expression = "within(@(test.annotation..*) *)"; AspectJExpressionPointcut springAnnotatedPc = testMatchAnnotationOnClass(expression); assertThat(springAnnotatedPc.matches(TestBean.class.getMethod("setName", String.class), TestBean.class)).isFalse(); @@ -381,7 +366,7 @@ public void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception } @Test - public void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { + void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { String expression = "within(@(test.annotation.transaction.*) *)"; testMatchAnnotationOnClass(expression); } @@ -399,7 +384,7 @@ private AspectJExpressionPointcut testMatchAnnotationOnClass(String expression) } @Test - public void testAnnotationOnMethodWithFQN() throws Exception { + void testAnnotationOnMethodWithFQN() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -413,7 +398,7 @@ public void testAnnotationOnMethodWithFQN() throws Exception { } @Test - public void testAnnotationOnCglibProxyMethod() throws Exception { + void testAnnotationOnCglibProxyMethod() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -425,7 +410,19 @@ public void testAnnotationOnCglibProxyMethod() throws Exception { } @Test - public void testAnnotationOnDynamicProxyMethod() throws Exception { + void testNotAnnotationOnCglibProxyMethod() throws Exception { + String expression = "!@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(true); + BeanA proxy = (BeanA) factory.getProxy(); + assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), proxy.getClass())).isFalse(); + } + + @Test + void testAnnotationOnDynamicProxyMethod() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -437,7 +434,19 @@ public void testAnnotationOnDynamicProxyMethod() throws Exception { } @Test - public void testAnnotationOnMethodWithWildcard() throws Exception { + void testNotAnnotationOnDynamicProxyMethod() throws Exception { + String expression = "!@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(false); + IBeanA proxy = (IBeanA) factory.getProxy(); + assertThat(ajexp.matches(IBeanA.class.getMethod("getAge"), proxy.getClass())).isFalse(); + } + + @Test + void testAnnotationOnMethodWithWildcard() throws Exception { String expression = "execution(@(test.annotation..*) * *(..))"; AspectJExpressionPointcut anySpringMethodAnnotation = new AspectJExpressionPointcut(); anySpringMethodAnnotation.setExpression(expression); @@ -453,7 +462,7 @@ public void testAnnotationOnMethodWithWildcard() throws Exception { } @Test - public void testAnnotationOnMethodArgumentsWithFQN() throws Exception { + void testAnnotationOnMethodArgumentsWithFQN() throws Exception { String expression = "@args(*, test.annotation.EmptySpringAnnotation))"; AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); takesSpringAnnotatedArgument2.setExpression(expression); @@ -482,7 +491,7 @@ public void testAnnotationOnMethodArgumentsWithFQN() throws Exception { } @Test - public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { + void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { String expression = "execution(* *(*, @(test..*) *))"; AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); takesSpringAnnotatedArgument2.setExpression(expression); @@ -505,6 +514,15 @@ public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { } + public static class OtherIOther implements IOther { + + @Override + public void absquatulate() { + // Empty + } + } + + public static class HasGeneric { public void setFriends(List friends) { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java index 3fd1b1e0c844..3d61e242897b 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ * @author Ramnivas Laddad * @author Chris Beams */ -public class BeanNamePointcutMatchingTests { +class BeanNamePointcutMatchingTests { @Test - public void testMatchingPointcuts() { + void testMatchingPointcuts() { assertMatch("someName", "bean(someName)"); // Spring bean names are less restrictive compared to AspectJ names (methods, types etc.) @@ -66,7 +66,7 @@ public void testMatchingPointcuts() { } @Test - public void testNonMatchingPointcuts() { + void testNonMatchingPointcuts() { assertMisMatch("someName", "bean(someNamex)"); assertMisMatch("someName", "bean(someX*Name)"); @@ -87,7 +87,6 @@ private void assertMisMatch(String beanName, String pcExpression) { } private static boolean matches(final String beanName, String pcExpression) { - @SuppressWarnings("serial") AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut() { @Override protected String getCurrentProxiedBeanName() { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java index b465a4625142..50559b43a2e6 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,20 +46,20 @@ * @author Ramnivas Laddad * @since 2.0 */ -public class MethodInvocationProceedingJoinPointTests { +class MethodInvocationProceedingJoinPointTests { @Test - public void testingBindingWithJoinPoint() { + void testingBindingWithJoinPoint() { assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); } @Test - public void testingBindingWithProceedingJoinPoint() { + void testingBindingWithProceedingJoinPoint() { assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); } @Test - public void testCanGetMethodSignatureFromJoinPoint() { + void testCanGetMethodSignatureFromJoinPoint() { final Object raw = new TestBean(); // Will be set by advice during a method call final int newAge = 23; @@ -118,7 +118,7 @@ public void testCanGetMethodSignatureFromJoinPoint() { } @Test - public void testCanGetSourceLocationFromJoinPoint() { + void testCanGetSourceLocationFromJoinPoint() { final Object raw = new TestBean(); ProxyFactory pf = new ProxyFactory(raw); pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); @@ -135,7 +135,7 @@ public void testCanGetSourceLocationFromJoinPoint() { } @Test - public void testCanGetStaticPartFromJoinPoint() { + void testCanGetStaticPartFromJoinPoint() { final Object raw = new TestBean(); ProxyFactory pf = new ProxyFactory(raw); pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); @@ -152,7 +152,7 @@ public void testCanGetStaticPartFromJoinPoint() { } @Test - public void toShortAndLongStringFormedCorrectly() throws Exception { + void toShortAndLongStringFormedCorrectly() { final Object raw = new TestBean(); ProxyFactory pf = new ProxyFactory(raw); pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java index 9ce2fdc69134..d7dec7bf33d1 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,17 +40,17 @@ /** * @author Dave Syer */ -public class TrickyAspectJPointcutExpressionTests { +class TrickyAspectJPointcutExpressionTests { @Test - public void testManualProxyJavaWithUnconditionalPointcut() throws Exception { + void testManualProxyJavaWithUnconditionalPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); testAdvice(new DefaultPointcutAdvisor(logAdvice), logAdvice, target, "TestServiceImpl"); } @Test - public void testManualProxyJavaWithStaticPointcut() throws Exception { + void testManualProxyJavaWithStaticPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -59,7 +59,7 @@ public void testManualProxyJavaWithStaticPointcut() throws Exception { } @Test - public void testManualProxyJavaWithDynamicPointcut() throws Exception { + void testManualProxyJavaWithDynamicPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -68,7 +68,7 @@ public void testManualProxyJavaWithDynamicPointcut() throws Exception { } @Test - public void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() throws Exception { + void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -77,7 +77,7 @@ public void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() throws E } @Test - public void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exception { + void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exception { LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -95,13 +95,12 @@ public void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exc testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, other, "TestServiceImpl"); } - private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) - throws Exception { + private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) { testAdvice(advisor, logAdvice, target, message, false); } private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message, - boolean proxyTargetClass) throws Exception { + boolean proxyTargetClass) { logAdvice.reset(); @@ -148,7 +147,7 @@ public TestException(String string) { public interface TestService { - public String sayHello(); + String sayHello(); } @@ -162,14 +161,14 @@ public String sayHello() { } - public class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { + public static class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { private int countBefore = 0; private int countThrows = 0; @Override - public void before(Method method, Object[] objects, @Nullable Object o) throws Throwable { + public void before(Method method, Object[] objects, @Nullable Object o) { countBefore++; } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java index 43373063622c..0a44be6add73 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for the {@link TypePatternClassFilter} class. + * Tests for {@link TypePatternClassFilter}. * * @author Rod Johnson * @author Rick Evans @@ -51,7 +51,7 @@ void invalidPattern() { } @Test - void invocationOfMatchesMethodBlowsUpWhenNoTypePatternHasBeenSet() throws Exception { + void invocationOfMatchesMethodBlowsUpWhenNoTypePatternHasBeenSet() { assertThatIllegalStateException().isThrownBy(() -> new TypePatternClassFilter().matches(String.class)); } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java index 978cd0495407..02d968212d53 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,6 @@ import org.aspectj.lang.annotation.DeclarePrecedence; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.aop.Advisor; @@ -84,15 +83,15 @@ abstract class AbstractAspectJAdvisorFactoryTests { @Test void rejectsPerCflowAspect() { assertThatExceptionOfType(AopConfigException.class) - .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean"))) - .withMessageContaining("PERCFLOW"); + .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean"))) + .withMessageContaining("PERCFLOW"); } @Test void rejectsPerCflowBelowAspect() { assertThatExceptionOfType(AopConfigException.class) - .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) - .withMessageContaining("PERCFLOWBELOW"); + .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) + .withMessageContaining("PERCFLOWBELOW"); } @Test @@ -363,7 +362,7 @@ void introductionOnTargetImplementingInterface() { assertThat(lockable.locked()).as("Already locked").isTrue(); lockable.lock(); assertThat(lockable.locked()).as("Real target ignores locking").isTrue(); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> lockable.unlock()); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(lockable::unlock); } @Test @@ -389,9 +388,7 @@ void introductionBasedOnAnnotationMatch() { // gh-9980 assertThat(lockable.locked()).isTrue(); } - // TODO: Why does this test fail? It hasn't been run before, so it maybe never actually passed... @Test - @Disabled void introductionWithArgumentBinding() { TestBean target = new TestBean(); @@ -648,7 +645,7 @@ void getAge() { static class NamedPointcutAspectWithFQN { @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.CommonPointcuts.getAge()()") int changeReturnValue(ProceedingJoinPoint pjp) { @@ -765,7 +762,7 @@ Object echo(Object obj) throws Exception { @Aspect - class DoublingAspect { + static class DoublingAspect { @Around("execution(* getAge())") public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { @@ -773,8 +770,14 @@ public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { } } + @Aspect - class IncrementingAspect extends DoublingAspect { + static class IncrementingAspect extends DoublingAspect { + + @Override + public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { + return ((int) pjp.proceed()) * 2; + } @Around("execution(* getAge())") public int incrementAge(ProceedingJoinPoint pjp) throws Throwable { @@ -783,7 +786,6 @@ public int incrementAge(ProceedingJoinPoint pjp) throws Throwable { } - @Aspect private static class InvocationTrackingAspect { @@ -1083,7 +1085,7 @@ class PerThisAspect { // Just to check that this doesn't cause problems with introduction processing @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Around("execution(int *.getAge())") int returnCountAsAge() { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java index 4b1271816898..8381a2ba16ea 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,26 +42,38 @@ */ class ArgumentBindingTests { + @Test + void annotationArgumentNameBinding() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TransactionalBean()); + proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); + + assertThatIllegalStateException() + .isThrownBy(proxiedTestBean::doInTransaction) + .withMessage("Invoked with @Transactional"); + } + @Test void bindingInPointcutUsedByAdvice() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(NamedPointcutWithArgs.class); - ITestBean proxiedTestBean = proxyFactory.getProxy(); + assertThatIllegalArgumentException() - .isThrownBy(() -> proxiedTestBean.setName("enigma")) - .withMessage("enigma"); + .isThrownBy(() -> proxiedTestBean.setName("enigma")) + .withMessage("enigma"); } @Test - void annotationArgumentNameBinding() { - AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TransactionalBean()); - proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + void bindingWithDynamicAdvice() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(DynamicPointcutWithArgs.class); + ITestBean proxiedTestBean = proxyFactory.getProxy(); - ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); - assertThatIllegalStateException() - .isThrownBy(proxiedTestBean::doInTransaction) - .withMessage("Invoked with @Transactional"); + proxiedTestBean.applyName(1); + assertThatIllegalArgumentException() + .isThrownBy(() -> proxiedTestBean.applyName("enigma")) + .withMessage("enigma"); } @Test @@ -94,6 +106,7 @@ public void doInTransaction() { } } + /** * Mimics Spring's @Transactional annotation without actually introducing the dependency. */ @@ -101,16 +114,17 @@ public void doInTransaction() { @interface Transactional { } + @Aspect static class PointcutWithAnnotationArgument { - @Around(value = "execution(* org.springframework..*.*(..)) && @annotation(transactional)") - public Object around(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable { + @Around("execution(* org.springframework..*.*(..)) && @annotation(transactional)") + public Object around(ProceedingJoinPoint pjp, Transactional transactional) { throw new IllegalStateException("Invoked with @Transactional"); } - } + @Aspect static class NamedPointcutWithArgs { @@ -118,10 +132,19 @@ static class NamedPointcutWithArgs { public void pointcutWithArgs(String s) {} @Around("pointcutWithArgs(aString)") - public Object doAround(ProceedingJoinPoint pjp, String aString) throws Throwable { + public Object doAround(ProceedingJoinPoint pjp, String aString) { throw new IllegalArgumentException(aString); } + } + + @Aspect("pertarget(execution(* *(..)))") + static class DynamicPointcutWithArgs { + + @Around("execution(* *(..)) && args(java.lang.String)") + public Object doAround(ProceedingJoinPoint pjp) { + throw new IllegalArgumentException(String.valueOf(pjp.getArgs()[0])); + } } } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java new file mode 100644 index 000000000000..124b1612acd5 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.aspectj.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; + +/** + * Tests for {@link AspectJAdvisorBeanRegistrationAotProcessor}. + * + * @author Sebastien Deleuze + * @since 6.1 + */ +class AspectJAdvisorBeanRegistrationAotProcessorTests { + + private final GenerationContext generationContext = new TestGenerationContext(); + + private final RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); + + + @Test + void shouldProcessAspectJClass() { + process(AspectJClass.class); + assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.DECLARED_FIELDS)) + .accepts(this.runtimeHints); + } + + @Test + void shouldSkipRegularClass() { + process(RegularClass.class); + assertThat(this.runtimeHints.reflection().typeHints()).isEmpty(); + } + + void process(Class beanClass) { + BeanRegistrationAotContribution contribution = createContribution(beanClass); + if (contribution != null) { + contribution.applyTo(this.generationContext, mock()); + } + } + + @Nullable + private static BeanRegistrationAotContribution createContribution(Class beanClass) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); + return new AspectJAdvisorBeanRegistrationAotProcessor() + .processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName())); + } + + + static class AspectJClass { + private static java.lang.Throwable ajc$initFailureCause; + } + + static class RegularClass { + private static java.lang.Throwable initFailureCause; + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java index 15203879aad2..79a67904814b 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,9 @@ import org.springframework.aop.Pointcut; import org.springframework.aop.aspectj.AspectJExpressionPointcut; -import org.springframework.aop.aspectj.AspectJExpressionPointcutTests; import org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.ExceptionThrowingAspect; import org.springframework.aop.framework.AopConfigException; +import org.springframework.aop.testfixture.aspectj.CommonExpressions; import org.springframework.aop.testfixture.aspectj.PerTargetAspect; import org.springframework.beans.testfixture.beans.TestBean; @@ -33,15 +33,15 @@ * @author Rod Johnson * @author Chris Beams */ -public class AspectJPointcutAdvisorTests { +class AspectJPointcutAdvisorTests { private final AspectJAdvisorFactory af = new ReflectiveAspectJAdvisorFactory(); @Test - public void testSingleton() throws SecurityException, NoSuchMethodException { + void testSingleton() throws SecurityException, NoSuchMethodException { AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(AspectJExpressionPointcutTests.MATCH_ALL_METHODS); + ajexp.setExpression(CommonExpressions.MATCH_ALL_METHODS); InstantiationModelAwarePointcutAdvisorImpl ajpa = new InstantiationModelAwarePointcutAdvisorImpl( ajexp, TestBean.class.getMethod("getAge"), af, @@ -53,9 +53,9 @@ public void testSingleton() throws SecurityException, NoSuchMethodException { } @Test - public void testPerTarget() throws SecurityException, NoSuchMethodException { + void testPerTarget() throws SecurityException, NoSuchMethodException { AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(AspectJExpressionPointcutTests.MATCH_ALL_METHODS); + ajexp.setExpression(CommonExpressions.MATCH_ALL_METHODS); InstantiationModelAwarePointcutAdvisorImpl ajpa = new InstantiationModelAwarePointcutAdvisorImpl( ajexp, TestBean.class.getMethod("getAge"), af, @@ -76,13 +76,13 @@ public void testPerTarget() throws SecurityException, NoSuchMethodException { } @Test - public void testPerCflowTarget() { + void testPerCflowTarget() { assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowAspect.class)); } @Test - public void testPerCflowBelowTarget() { + void testPerCflowBelowTarget() { assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowBelowAspect.class)); } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java index 3fb1b05d81b9..1b3557eae2dd 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,17 +36,17 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class AspectProxyFactoryTests { +class AspectProxyFactoryTests { @Test - public void testWithNonAspect() { + void testWithNonAspect() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); assertThatIllegalArgumentException().isThrownBy(() -> proxyFactory.addAspect(TestBean.class)); } @Test - public void testWithSimpleAspect() throws Exception { + void testWithSimpleAspect() { TestBean bean = new TestBean(); bean.setAge(2); AspectJProxyFactory proxyFactory = new AspectJProxyFactory(bean); @@ -56,7 +56,7 @@ public void testWithSimpleAspect() throws Exception { } @Test - public void testWithPerThisAspect() throws Exception { + void testWithPerThisAspect() { TestBean bean1 = new TestBean(); TestBean bean2 = new TestBean(); @@ -76,7 +76,7 @@ public void testWithPerThisAspect() throws Exception { } @Test - public void testWithInstanceWithNonAspect() throws Exception { + void testWithInstanceWithNonAspect() { AspectJProxyFactory pf = new AspectJProxyFactory(); assertThatIllegalArgumentException().isThrownBy(() -> pf.addAspect(new TestBean())); @@ -84,7 +84,7 @@ public void testWithInstanceWithNonAspect() throws Exception { @Test @SuppressWarnings("unchecked") - public void testSerializable() throws Exception { + void testSerializable() throws Exception { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnVarargs.class); ITestBean proxy = proxyFactory.getProxy(); @@ -94,7 +94,7 @@ public void testSerializable() throws Exception { } @Test - public void testWithInstance() throws Exception { + void testWithInstance() throws Exception { MultiplyReturnValue aspect = new MultiplyReturnValue(); int multiple = 3; aspect.setMultiple(multiple); @@ -113,14 +113,13 @@ public void testWithInstance() throws Exception { } @Test - public void testWithNonSingletonAspectInstance() throws Exception { + void testWithNonSingletonAspectInstance() { AspectJProxyFactory pf = new AspectJProxyFactory(); assertThatIllegalArgumentException().isThrownBy(() -> pf.addAspect(new PerThisAspect())); } @Test // SPR-13328 - @SuppressWarnings("unchecked") - public void testProxiedVarargsWithEnumArray() throws Exception { + public void testProxiedVarargsWithEnumArray() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnVarargs.class); ITestBean proxy = proxyFactory.getProxy(); @@ -128,8 +127,7 @@ public void testProxiedVarargsWithEnumArray() throws Exception { } @Test // SPR-13328 - @SuppressWarnings("unchecked") - public void testUnproxiedVarargsWithEnumArray() throws Exception { + public void testUnproxiedVarargsWithEnumArray() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnSetter.class); ITestBean proxy = proxyFactory.getProxy(); @@ -174,13 +172,13 @@ public interface MyInterface { public enum MyEnum implements MyInterface { - A, B; + A, B } public enum MyOtherEnum implements MyInterface { - C, D; + C, D } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java index a6ecf37304de..f12c6b982a43 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ * @author Rob Harrop * @author Chris Beams */ -public class AspectJNamespaceHandlerTests { +class AspectJNamespaceHandlerTests { private ParserContext parserContext; @@ -47,7 +47,7 @@ public class AspectJNamespaceHandlerTests { @BeforeEach - public void setUp() throws Exception { + public void setUp() { SourceExtractor sourceExtractor = new PassThroughSourceExtractor(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.registry); XmlReaderContext readerContext = @@ -56,7 +56,7 @@ public void setUp() throws Exception { } @Test - public void testRegisterAutoProxyCreator() throws Exception { + void testRegisterAutoProxyCreator() { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); @@ -65,7 +65,7 @@ public void testRegisterAutoProxyCreator() throws Exception { } @Test - public void testRegisterAspectJAutoProxyCreator() throws Exception { + void testRegisterAspectJAutoProxyCreator() { AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); @@ -77,7 +77,7 @@ public void testRegisterAspectJAutoProxyCreator() throws Exception { } @Test - public void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() throws Exception { + void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).isEqualTo(1); @@ -89,7 +89,7 @@ public void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() th } @Test - public void testRegisterAutoProxyCreatorWhenAspectJAutoProxyCreatorAlreadyExists() throws Exception { + void testRegisterAutoProxyCreatorWhenAspectJAutoProxyCreatorAlreadyExists() { AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).isEqualTo(1); diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java index d391d45f39bd..118828f338b4 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ * @author Adrian Colyer * @author Chris Beams */ -public class AspectJPrecedenceComparatorTests { +class AspectJPrecedenceComparatorTests { private static final int HIGH_PRECEDENCE_ADVISOR_ORDER = 100; private static final int LOW_PRECEDENCE_ADVISOR_ORDER = 200; @@ -56,7 +56,7 @@ public class AspectJPrecedenceComparatorTests { @BeforeEach - public void setUp() throws Exception { + public void setUp() { this.comparator = new AspectJPrecedenceComparator(); this.anyOldMethod = getClass().getMethods()[0]; this.anyOldPointcut = new AspectJExpressionPointcut(); @@ -65,7 +65,7 @@ public void setUp() throws Exception { @Test - public void testSameAspectNoAfterAdvice() { + void testSameAspectNoAfterAdvice() { Advisor advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); @@ -76,7 +76,7 @@ public void testSameAspectNoAfterAdvice() { } @Test - public void testSameAspectAfterAdvice() { + void testSameAspectAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor2 sorted before advisor1").isEqualTo(1); @@ -87,14 +87,14 @@ public void testSameAspectAfterAdvice() { } @Test - public void testSameAspectOneOfEach() { + void testSameAspectOneOfEach() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 and advisor2 not comparable").isEqualTo(1); } @Test - public void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { + void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { Advisor advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); @@ -105,7 +105,7 @@ public void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { } @Test - public void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { + void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); @@ -116,7 +116,7 @@ public void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { } @Test - public void testHigherAdvisorPrecedenceNoAfterAdvice() { + void testHigherAdvisorPrecedenceNoAfterAdvice() { Advisor advisor1 = createSpringAOPBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER); Advisor advisor2 = createAspectJBeforeAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); @@ -127,7 +127,7 @@ public void testHigherAdvisorPrecedenceNoAfterAdvice() { } @Test - public void testHigherAdvisorPrecedenceAfterAdvice() { + void testHigherAdvisorPrecedenceAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); @@ -138,7 +138,7 @@ public void testHigherAdvisorPrecedenceAfterAdvice() { } @Test - public void testLowerAdvisorPrecedenceNoAfterAdvice() { + void testLowerAdvisorPrecedenceNoAfterAdvice() { Advisor advisor1 = createAspectJBeforeAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); @@ -149,7 +149,7 @@ public void testLowerAdvisorPrecedenceNoAfterAdvice() { } @Test - public void testLowerAdvisorPrecedenceAfterAdvice() { + void testLowerAdvisorPrecedenceAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); diff --git a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java index ffb270cdd9b4..c6d60133c446 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,8 +45,8 @@ class AopNamespaceHandlerEventTests { private static final Class CLASS = AopNamespaceHandlerEventTests.class; - private static final Resource CONTEXT = qualifiedResource(CLASS, "context.xml"); - private static final Resource POINTCUT_EVENTS_CONTEXT = qualifiedResource(CLASS, "pointcutEvents.xml"); + private static final Resource CONTEXT = qualifiedResource(CLASS, "context.xml"); + private static final Resource POINTCUT_EVENTS_CONTEXT = qualifiedResource(CLASS, "pointcutEvents.xml"); private static final Resource POINTCUT_REF_CONTEXT = qualifiedResource(CLASS, "pointcutRefEvents.xml"); private static final Resource DIRECT_POINTCUT_EVENTS_CONTEXT = qualifiedResource(CLASS, "directPointcutEvents.xml"); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java index 959c4cd25a30..33326d426a6d 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,14 +25,14 @@ /** * Benchmarks for introductions. - * + *

* NOTE: No assertions! * * @author Rod Johnson * @author Chris Beams * @since 2.0 */ -public class IntroductionBenchmarkTests { +class IntroductionBenchmarkTests { private static final int EXPECTED_COMPARE = 13; @@ -53,7 +53,7 @@ public interface Counter { } @Test - public void timeManyInvocations() { + void timeManyInvocations() { StopWatch sw = new StopWatch(); TestBean target = new TestBean(); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java index 047e84b3d72f..abf36f2adda9 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,14 +29,14 @@ * * @author Dave Syer */ -public class NullPrimitiveTests { +class NullPrimitiveTests { interface Foo { int getValue(); } @Test - public void testNullPrimitiveWithJdkProxy() { + void testNullPrimitiveWithJdkProxy() { class SimpleFoo implements Foo { @Override @@ -51,8 +51,7 @@ public int getValue() { Foo foo = (Foo) factory.getProxy(); - assertThatExceptionOfType(AopInvocationException.class).isThrownBy(() -> - foo.getValue()) + assertThatExceptionOfType(AopInvocationException.class).isThrownBy(foo::getValue) .withMessageContaining("Foo.getValue()"); } @@ -63,7 +62,7 @@ public int getValue() { } @Test - public void testNullPrimitiveWithCglibProxy() { + void testNullPrimitiveWithCglibProxy() { Bar target = new Bar(); ProxyFactory factory = new ProxyFactory(target); @@ -71,8 +70,7 @@ public void testNullPrimitiveWithCglibProxy() { Bar bar = (Bar) factory.getProxy(); - assertThatExceptionOfType(AopInvocationException.class).isThrownBy(() -> - bar.getValue()) + assertThatExceptionOfType(AopInvocationException.class).isThrownBy(bar::getValue) .withMessageContaining("Bar.getValue()"); } diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java index d58a9b594a16..826191596593 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,13 +32,13 @@ * @author Chris Beams * @since 03.09.2004 */ -public class PrototypeTargetTests { +class PrototypeTargetTests { private static final Resource CONTEXT = qualifiedResource(PrototypeTargetTests.class, "context.xml"); @Test - public void testPrototypeProxyWithPrototypeTarget() { + void testPrototypeProxyWithPrototypeTarget() { TestBeanImpl.constructionCount = 0; DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); @@ -52,7 +52,7 @@ public void testPrototypeProxyWithPrototypeTarget() { } @Test - public void testSingletonProxyWithPrototypeTarget() { + void testSingletonProxyWithPrototypeTarget() { TestBeanImpl.constructionCount = 0; DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java index a0f52916504a..d7ec7136b50a 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,10 +57,10 @@ * @author Chris Beams * @since 14.05.2003 */ -public class ProxyFactoryTests { +class ProxyFactoryTests { @Test - public void testIndexOfMethods() { + void testIndexOfMethods() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -76,7 +76,7 @@ public void testIndexOfMethods() { } @Test - public void testRemoveAdvisorByReference() { + void testRemoveAdvisorByReference() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -96,7 +96,7 @@ public void testRemoveAdvisorByReference() { } @Test - public void testRemoveAdvisorByIndex() { + void testRemoveAdvisorByIndex() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -144,7 +144,7 @@ public void testRemoveAdvisorByIndex() { } @Test - public void testReplaceAdvisor() { + void testReplaceAdvisor() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -173,7 +173,7 @@ public void testReplaceAdvisor() { } @Test - public void testAddRepeatedInterface() { + void testAddRepeatedInterface() { TimeStamped tst = () -> { throw new UnsupportedOperationException("getTimeStamp"); }; @@ -186,7 +186,7 @@ public void testAddRepeatedInterface() { } @Test - public void testGetsAllInterfaces() { + void testGetsAllInterfaces() { // Extend to get new interface class TestBeanSubclass extends TestBean implements Comparable { @Override @@ -220,10 +220,10 @@ public int compareTo(Object arg0) { } @Test - public void testInterceptorInclusionMethods() { + void testInterceptorInclusionMethods() { class MyInterceptor implements MethodInterceptor { @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public Object invoke(MethodInvocation invocation) { throw new UnsupportedOperationException(); } } @@ -244,9 +244,9 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } @Test - public void testSealedInterfaceExclusion() { + void testSealedInterfaceExclusion() { // String implements ConstantDesc on JDK 12+, sealed as of JDK 17 - ProxyFactory factory = new ProxyFactory(new String()); + ProxyFactory factory = new ProxyFactory(""); NopInterceptor di = new NopInterceptor(); factory.addAdvice(0, di); Object proxy = factory.getProxy(); @@ -257,7 +257,7 @@ public void testSealedInterfaceExclusion() { * Should see effect immediately on behavior. */ @Test - public void testCanAddAndRemoveAspectInterfacesOnSingleton() { + void testCanAddAndRemoveAspectInterfacesOnSingleton() { ProxyFactory config = new ProxyFactory(new TestBean()); assertThat(config.getProxy()).as("Shouldn't implement TimeStamped before manipulation") @@ -304,7 +304,7 @@ public void testCanAddAndRemoveAspectInterfacesOnSingleton() { } @Test - public void testProxyTargetClassWithInterfaceAsTarget() { + void testProxyTargetClassWithInterfaceAsTarget() { ProxyFactory pf = new ProxyFactory(); pf.setTargetClass(ITestBean.class); Object proxy = pf.getProxy(); @@ -320,7 +320,7 @@ public void testProxyTargetClassWithInterfaceAsTarget() { } @Test - public void testProxyTargetClassWithConcreteClassAsTarget() { + void testProxyTargetClassWithConcreteClassAsTarget() { ProxyFactory pf = new ProxyFactory(); pf.setTargetClass(TestBean.class); Object proxy = pf.getProxy(); @@ -347,19 +347,18 @@ public void testExclusionOfNonPublicInterfaces() { } @Test - public void testInterfaceProxiesCanBeOrderedThroughAnnotations() { + void testInterfaceProxiesCanBeOrderedThroughAnnotations() { Object proxy1 = new ProxyFactory(new A()).getProxy(); Object proxy2 = new ProxyFactory(new B()).getProxy(); List list = new ArrayList<>(2); list.add(proxy1); list.add(proxy2); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isSameAs(proxy2); - assertThat(list.get(1)).isSameAs(proxy1); + assertThat(list).containsExactly(proxy2, proxy1); } @Test - public void testTargetClassProxiesCanBeOrderedThroughAnnotations() { + void testTargetClassProxiesCanBeOrderedThroughAnnotations() { ProxyFactory pf1 = new ProxyFactory(new A()); pf1.setProxyTargetClass(true); ProxyFactory pf2 = new ProxyFactory(new B()); @@ -370,12 +369,11 @@ public void testTargetClassProxiesCanBeOrderedThroughAnnotations() { list.add(proxy1); list.add(proxy2); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isSameAs(proxy2); - assertThat(list.get(1)).isSameAs(proxy1); + assertThat(list).containsExactly(proxy2, proxy1); } @Test - public void testInterceptorWithoutJoinpoint() { + void testInterceptorWithoutJoinpoint() { final TestBean target = new TestBean("tb"); ITestBean proxy = ProxyFactory.getProxy(ITestBean.class, (MethodInterceptor) invocation -> { assertThat(invocation.getThis()).isNull(); @@ -385,7 +383,7 @@ public void testInterceptorWithoutJoinpoint() { } @Test - public void testCharSequenceProxy() { + void testCharSequenceProxy() { CharSequence target = "test"; ProxyFactory pf = new ProxyFactory(target); ClassLoader cl = target.getClass().getClassLoader(); @@ -395,7 +393,7 @@ public void testCharSequenceProxy() { } @Test - public void testDateProxy() { + void testDateProxy() { Date target = new Date(); ProxyFactory pf = new ProxyFactory(target); pf.setProxyTargetClass(true); @@ -406,14 +404,14 @@ public void testDateProxy() { } @Test - public void testJdbcSavepointProxy() throws SQLException { + void testJdbcSavepointProxy() throws SQLException { Savepoint target = new Savepoint() { @Override - public int getSavepointId() throws SQLException { + public int getSavepointId() { return 1; } @Override - public String getSavepointName() throws SQLException { + public String getSavepointName() { return "sp"; } }; 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 0b89db0d52d2..1a669c018e0d 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,30 +23,31 @@ import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; +import org.springframework.aop.framework.AopConfigException; import org.springframework.aop.testfixture.advice.MyThrowsHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** * @author Rod Johnson * @author Chris Beams + * @author Juergen Hoeller */ -public class ThrowsAdviceInterceptorTests { +class ThrowsAdviceInterceptorTests { @Test - public void testNoHandlerMethods() { + void testNoHandlerMethods() { // should require one handler method at least - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> new ThrowsAdviceInterceptor(new Object())); } @Test - public void testNotInvoked() throws Throwable { + void testNotInvoked() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); Object ret = new Object(); @@ -57,7 +58,7 @@ public void testNotInvoked() throws Throwable { } @Test - public void testNoHandlerMethodForThrowable() throws Throwable { + void testNoHandlerMethodForThrowable() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); assertThat(ti.getHandlerMethodCount()).isEqualTo(2); @@ -69,7 +70,7 @@ public void testNoHandlerMethodForThrowable() throws Throwable { } @Test - public void testCorrectHandlerUsed() throws Throwable { + void testCorrectHandlerUsed() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); FileNotFoundException ex = new FileNotFoundException(); @@ -83,7 +84,7 @@ public void testCorrectHandlerUsed() throws Throwable { } @Test - public void testCorrectHandlerUsedForSubclass() throws Throwable { + void testCorrectHandlerUsedForSubclass() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); // Extends RemoteException @@ -96,10 +97,9 @@ public void testCorrectHandlerUsedForSubclass() throws Throwable { } @Test - public void testHandlerMethodThrowsException() throws Throwable { + void testHandlerMethodThrowsException() throws Throwable { final Throwable t = new Throwable(); - @SuppressWarnings("serial") MyThrowsHandler th = new MyThrowsHandler() { @Override public void afterThrowing(RemoteException ex) throws Throwable { diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java index d135689decca..616c855f3e32 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ * @author Chris Beams * @since 06.04.2004 */ -public class ConcurrencyThrottleInterceptorTests { +class ConcurrencyThrottleInterceptorTests { protected static final Log logger = LogFactory.getLog(ConcurrencyThrottleInterceptorTests.class); @@ -44,7 +44,7 @@ public class ConcurrencyThrottleInterceptorTests { @Test - public void testSerializable() throws Exception { + void testSerializable() throws Exception { DerivedTestBean tb = new DerivedTestBean(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setInterfaces(ITestBean.class); @@ -63,12 +63,12 @@ public void testSerializable() throws Exception { } @Test - public void testMultipleThreadsWithLimit1() { + void testMultipleThreadsWithLimit1() { testMultipleThreads(1); } @Test - public void testMultipleThreadsWithLimit10() { + void testMultipleThreadsWithLimit10() { testMultipleThreads(10); } @@ -95,7 +95,7 @@ private void testMultipleThreads(int concurrencyLimit) { ex.printStackTrace(); } threads[i] = new ConcurrencyThread(proxy, - i % 2 == 0 ? new OutOfMemoryError() : new IllegalStateException()); + (i % 2 == 0 ? new OutOfMemoryError() : new IllegalStateException())); threads[i].start(); } for (int i = 0; i < NR_OF_THREADS; i++) { @@ -111,8 +111,8 @@ private void testMultipleThreads(int concurrencyLimit) { private static class ConcurrencyThread extends Thread { - private ITestBean proxy; - private Throwable ex; + private final ITestBean proxy; + private final Throwable ex; public ConcurrencyThread(ITestBean proxy, Throwable ex) { this.proxy = proxy; diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java index 8b3be03d03b3..44523196bbbf 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,17 @@ package org.springframework.aop.interceptor; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -27,73 +34,80 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.ALLOWED_PLACEHOLDERS; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_NAME; +import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_SHORT_NAME; /** + * Tests for {@link CustomizableTraceInterceptor}. + * * @author Rob Harrop * @author Rick Evans * @author Juergen Hoeller * @author Chris Beams + * @author Sam Brannen */ -public class CustomizableTraceInterceptorTests { +class CustomizableTraceInterceptorTests { + + private final CustomizableTraceInterceptor interceptor = new CustomizableTraceInterceptor(); + @Test - public void testSetEmptyEnterMessage() { + void setEmptyEnterMessage() { // Must not be able to set empty enter message - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setEnterMessage("")); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage("")); } @Test - public void testSetEnterMessageWithReturnValuePlaceholder() { + void setEnterMessageWithReturnValuePlaceholder() { // Must not be able to set enter message with return value placeholder - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE)); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_RETURN_VALUE)); } @Test - public void testSetEnterMessageWithExceptionPlaceholder() { + void setEnterMessageWithExceptionPlaceholder() { // Must not be able to set enter message with exception placeholder - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION)); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_EXCEPTION)); } @Test - public void testSetEnterMessageWithInvocationTimePlaceholder() { + void setEnterMessageWithInvocationTimePlaceholder() { // Must not be able to set enter message with invocation time placeholder - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME)); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_INVOCATION_TIME)); } @Test - public void testSetEmptyExitMessage() { + void setEmptyExitMessage() { // Must not be able to set empty exit message - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setExitMessage("")); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExitMessage("")); } @Test - public void testSetExitMessageWithExceptionPlaceholder() { + void setExitMessageWithExceptionPlaceholder() { // Must not be able to set exit message with exception placeholder - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setExitMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION)); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExitMessage(PLACEHOLDER_EXCEPTION)); } @Test - public void testSetEmptyExceptionMessage() { + void setEmptyExceptionMessage() { // Must not be able to set empty exception message - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setExceptionMessage("")); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExceptionMessage("")); } @Test - public void testSetExceptionMethodWithReturnValuePlaceholder() { + void setExceptionMethodWithReturnValuePlaceholder() { // Must not be able to set exception message with return value placeholder - assertThatIllegalArgumentException().isThrownBy(() -> - new CustomizableTraceInterceptor().setExceptionMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE)); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExceptionMessage(PLACEHOLDER_RETURN_VALUE)); } @Test - public void testSunnyDayPathLogsCorrectly() throws Throwable { + void sunnyDayPathLogsCorrectly() throws Throwable { MethodInvocation methodInvocation = mock(); given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString")); given(methodInvocation.getThis()).willReturn(this); @@ -108,7 +122,7 @@ public void testSunnyDayPathLogsCorrectly() throws Throwable { } @Test - public void testExceptionPathLogsCorrectly() throws Throwable { + void exceptionPathLogsCorrectly() throws Throwable { MethodInvocation methodInvocation = mock(); IllegalArgumentException exception = new IllegalArgumentException(); @@ -120,18 +134,17 @@ public void testExceptionPathLogsCorrectly() throws Throwable { given(log.isTraceEnabled()).willReturn(true); CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log); - assertThatIllegalArgumentException().isThrownBy(() -> - interceptor.invoke(methodInvocation)); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.invoke(methodInvocation)); verify(log).trace(anyString()); verify(log).trace(anyString(), eq(exception)); } @Test - public void testSunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable { + void sunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable { MethodInvocation methodInvocation = mock(); - given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString")); given(methodInvocation.getThis()).willReturn(this); given(methodInvocation.getArguments()).willReturn(new Object[]{"$ One \\$", 2L}); given(methodInvocation.proceed()).willReturn("Hello!"); @@ -141,31 +154,58 @@ public void testSunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log); interceptor.setEnterMessage(new StringBuilder() - .append("Entering the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME) - .append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_NAME) - .append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS) - .append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES) + .append("Entering the '").append(PLACEHOLDER_METHOD_NAME) + .append("' method of the [").append(PLACEHOLDER_TARGET_CLASS_NAME) + .append("] class with the following args (").append(PLACEHOLDER_ARGUMENTS) + .append(") and arg types (").append(PLACEHOLDER_ARGUMENT_TYPES) .append(").").toString()); interceptor.setExitMessage(new StringBuilder() - .append("Exiting the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME) - .append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_SHORT_NAME) - .append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS) - .append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES) - .append("), returning '").append(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE) - .append("' and taking '").append(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME) + .append("Exiting the '").append(PLACEHOLDER_METHOD_NAME) + .append("' method of the [").append(PLACEHOLDER_TARGET_CLASS_SHORT_NAME) + .append("] class with the following args (").append(PLACEHOLDER_ARGUMENTS) + .append(") and arg types (").append(PLACEHOLDER_ARGUMENT_TYPES) + .append("), returning '").append(PLACEHOLDER_RETURN_VALUE) + .append("' and taking '").append(PLACEHOLDER_INVOCATION_TIME) .append("' this long.").toString()); interceptor.invoke(methodInvocation); verify(log, times(2)).trace(anyString()); } + /** + * This test effectively verifies that the internal ALLOWED_PLACEHOLDERS set + * is properly configured in {@link CustomizableTraceInterceptor}. + */ + @Test + void supportedPlaceholderValues() { + assertThat(ALLOWED_PLACEHOLDERS).containsExactlyInAnyOrderElementsOf(getPlaceholderConstantValues()); + } + + private List getPlaceholderConstantValues() { + return Arrays.stream(CustomizableTraceInterceptor.class.getFields()) + .filter(ReflectionUtils::isPublicStaticFinal) + .filter(field -> field.getName().startsWith("PLACEHOLDER_")) + .map(this::getFieldValue) + .map(String.class::cast) + .toList(); + } + + private Object getFieldValue(Field field) { + try { + return field.get(null); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + @SuppressWarnings("serial") private static class StubCustomizableTraceInterceptor extends CustomizableTraceInterceptor { private final Log log; - public StubCustomizableTraceInterceptor(Log log) { + StubCustomizableTraceInterceptor(Log log) { super.setUseDynamicLogger(false); this.log = log; } diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java index abdfd1200372..0c462a7c86a2 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,15 +30,15 @@ import static org.mockito.Mockito.verify; /** - * Unit tests for the {@link DebugInterceptor} class. + * Tests for {@link DebugInterceptor}. * * @author Rick Evans * @author Chris Beams */ -public class DebugInterceptorTests { +class DebugInterceptorTests { @Test - public void testSunnyDayPathLogsCorrectly() throws Throwable { + void testSunnyDayPathLogsCorrectly() throws Throwable { MethodInvocation methodInvocation = mock(); Log log = mock(); @@ -52,7 +52,7 @@ public void testSunnyDayPathLogsCorrectly() throws Throwable { } @Test - public void testExceptionPathStillLogsCorrectly() throws Throwable { + void testExceptionPathStillLogsCorrectly() throws Throwable { MethodInvocation methodInvocation = mock(); IllegalArgumentException exception = new IllegalArgumentException(); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java index 9bd43d1aef3f..565e3e005a42 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,9 @@ * @author Rod Johnson * @author Chris Beams */ -public class ExposeBeanNameAdvisorsTests { +class ExposeBeanNameAdvisorsTests { - private class RequiresBeanNameBoundTestBean extends TestBean { + private static class RequiresBeanNameBoundTestBean extends TestBean { private final String beanName; public RequiresBeanNameBoundTestBean(String beanName) { @@ -46,7 +46,7 @@ public int getAge() { } @Test - public void testNoIntroduction() { + void testNoIntroduction() { String beanName = "foo"; TestBean target = new RequiresBeanNameBoundTestBean(beanName); ProxyFactory pf = new ProxyFactory(target); @@ -61,7 +61,7 @@ public void testNoIntroduction() { } @Test - public void testWithIntroduction() { + void testWithIntroduction() { String beanName = "foo"; TestBean target = new RequiresBeanNameBoundTestBean(beanName); ProxyFactory pf = new ProxyFactory(target); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java index 331dc2b86809..79726a94b4d0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,10 +31,10 @@ * @author Rod Johnson * @author Chris Beams */ -public class ExposeInvocationInterceptorTests { +class ExposeInvocationInterceptorTests { @Test - public void testXmlConfig() { + void testXmlConfig() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ExposeInvocationInterceptorTests.class, "context.xml")); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java index 76cada1837f9..6cc67b4da573 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * @author Rick Evans * @author Chris Beams */ -public class PerformanceMonitorInterceptorTests { +class PerformanceMonitorInterceptorTests { @Test - public void testSuffixAndPrefixAssignment() { + void testSuffixAndPrefixAssignment() { PerformanceMonitorInterceptor interceptor = new PerformanceMonitorInterceptor(); assertThat(interceptor.getPrefix()).isNotNull(); @@ -49,9 +49,9 @@ public void testSuffixAndPrefixAssignment() { } @Test - public void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { + void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { MethodInvocation mi = mock(); - given(mi.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); Log log = mock(); @@ -62,10 +62,10 @@ public void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { } @Test - public void testExceptionPathStillLogsPerformanceMetricsCorrectly() throws Throwable { + void testExceptionPathStillLogsPerformanceMetricsCorrectly() throws Throwable { MethodInvocation mi = mock(); - given(mi.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); given(mi.proceed()).willThrow(new IllegalArgumentException()); Log log = mock(); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java index ee96500febe1..b977f97a4c00 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,15 +29,15 @@ import static org.mockito.Mockito.verify; /** - * Unit tests for the {@link SimpleTraceInterceptor} class. + * Tests for {@link SimpleTraceInterceptor}. * * @author Rick Evans * @author Chris Beams */ -public class SimpleTraceInterceptorTests { +class SimpleTraceInterceptorTests { @Test - public void testSunnyDayPathLogsCorrectly() throws Throwable { + void testSunnyDayPathLogsCorrectly() throws Throwable { MethodInvocation mi = mock(); given(mi.getMethod()).willReturn(String.class.getMethod("toString")); given(mi.getThis()).willReturn(this); @@ -51,7 +51,7 @@ public void testSunnyDayPathLogsCorrectly() throws Throwable { } @Test - public void testExceptionPathStillLogsCorrectly() throws Throwable { + void testExceptionPathStillLogsCorrectly() throws Throwable { MethodInvocation mi = mock(); given(mi.getMethod()).willReturn(String.class.getMethod("toString")); given(mi.getThis()).willReturn(this); diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java index 8affdf3bd3d6..ee21418518b5 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,36 +24,36 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for the {@link DefaultScopedObject} class. + * Tests for {@link DefaultScopedObject}. * * @author Rick Evans * @author Chris Beams */ -public class DefaultScopedObjectTests { +class DefaultScopedObjectTests { private static final String GOOD_BEAN_NAME = "foo"; @Test - public void testCtorWithNullBeanFactory() throws Exception { + void testCtorWithNullBeanFactory() { assertThatIllegalArgumentException().isThrownBy(() -> new DefaultScopedObject(null, GOOD_BEAN_NAME)); } @Test - public void testCtorWithNullTargetBeanName() throws Exception { + void testCtorWithNullTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName(null)); } @Test - public void testCtorWithEmptyTargetBeanName() throws Exception { + void testCtorWithEmptyTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName("")); } @Test - public void testCtorWithJustWhitespacedTargetBeanName() throws Exception { + void testCtorWithJustWhitespacedTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName(" ")); } diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java index a746532495b0..0a8b727a9435 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,16 +31,16 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class ScopedProxyAutowireTests { +class ScopedProxyAutowireTests { @Test - public void testScopedProxyInheritsAutowireCandidateFalse() { + void testScopedProxyInheritsAutowireCandidateFalse() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireFalse.xml")); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false)).contains("scoped")).isTrue(); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false)).contains("scoped")).isTrue(); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false))).contains("scoped"); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false))).contains("scoped"); assertThat(bf.containsSingleton("scoped")).isFalse(); TestBean autowired = (TestBean) bf.getBean("autowired"); TestBean unscoped = (TestBean) bf.getBean("unscoped"); @@ -48,13 +48,13 @@ public void testScopedProxyInheritsAutowireCandidateFalse() { } @Test - public void testScopedProxyReplacesAutowireCandidateTrue() { + void testScopedProxyReplacesAutowireCandidateTrue() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireTrue.xml")); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false)).contains("scoped")).isTrue(); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false)).contains("scoped")).isTrue(); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false))).contains("scoped"); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false))).contains("scoped"); assertThat(bf.containsSingleton("scoped")).isFalse(); TestBean autowired = (TestBean) bf.getBean("autowired"); TestBean scoped = (TestBean) bf.getBean("scoped"); diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java index b69616f49757..ff28009a7fb6 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link ScopedProxyUtils}. + * Tests for {@link ScopedProxyUtils}. * * @author Sam Brannen * @since 5.1.10 diff --git a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java index dc5437e6cd67..fb4d1ed6f717 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,29 +17,35 @@ package org.springframework.aop.support; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.aop.testfixture.interceptor.NopInterceptor; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.ResolvableType; import org.springframework.core.testfixture.io.SerializationTestUtils; import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; /** * @author Rod Johnson * @author Chris Beams + * @author Sebastien Deleuze + * @author Juergen Hoeller */ -public class AopUtilsTests { +class AopUtilsTests { @Test - public void testPointcutCanNeverApply() { + void testPointcutCanNeverApply() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazzy) { @@ -52,13 +58,13 @@ public boolean matches(Method method, @Nullable Class clazzy) { } @Test - public void testPointcutAlwaysApplies() { + void testPointcutAlwaysApplies() { assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue(); assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue(); } @Test - public void testPointcutAppliesToOneMethodOnObject() { + void testPointcutAppliesToOneMethodOnObject() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazz) { @@ -78,7 +84,7 @@ public boolean matches(Method method, @Nullable Class clazz) { * that's subverted the singleton construction limitation. */ @Test - public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { + void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { assertThat(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)).isSameAs(MethodMatcher.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(ClassFilter.TRUE)).isSameAs(ClassFilter.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcut.TRUE)).isSameAs(Pointcut.TRUE); @@ -88,4 +94,45 @@ public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throw assertThat(SerializationTestUtils.serializeAndDeserialize(ExposeInvocationInterceptor.INSTANCE)).isSameAs(ExposeInvocationInterceptor.INSTANCE); } + @Test + void testInvokeJoinpointUsingReflection() throws Throwable { + String name = "foo"; + TestBean testBean = new TestBean(name); + Method method = ReflectionUtils.findMethod(TestBean.class, "getName"); + Object result = AopUtils.invokeJoinpointUsingReflection(testBean, method, new Object[0]); + assertThat(result).isEqualTo(name); + } + + @Test // gh-32365 + void mostSpecificMethodBetweenJdkProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithInterface()).getProxyClass(getClass().getClassLoader()); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + @Test // gh-32365 + void mostSpecificMethodBetweenCglibProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithoutInterface()).getProxyClass(getClass().getClassLoader()); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithoutInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + + interface ProxyInterface { + + void handle(List list); + } + + static class WithInterface implements ProxyInterface { + + public void handle(List list) { + } + } + + static class WithoutInterface { + + public void handle(List list) { + } + } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java index db83a9424df1..f85b93bfcb82 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,12 @@ import org.springframework.core.NestedRuntimeException; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** - * Unit tests for {@link ClassFilters}. + * Tests for {@link ClassFilters}. * * @author Rod Johnson * @author Chris Beams @@ -66,4 +69,65 @@ void intersection() { .matches("^.+IntersectionClassFilter: \\[.+RootClassFilter: .+Exception, .+RootClassFilter: .+NestedRuntimeException\\]$"); } + @Test + void negateClassFilter() { + ClassFilter filter = mock(ClassFilter.class); + given(filter.matches(String.class)).willReturn(true); + ClassFilter negate = ClassFilters.negate(filter); + assertThat(negate.matches(String.class)).isFalse(); + verify(filter).matches(String.class); + } + + @Test + void negateTrueClassFilter() { + ClassFilter negate = ClassFilters.negate(ClassFilter.TRUE); + assertThat(negate.matches(String.class)).isFalse(); + assertThat(negate.matches(Object.class)).isFalse(); + assertThat(negate.matches(Integer.class)).isFalse(); + } + + @Test + void negateTrueClassFilterAppliedTwice() { + ClassFilter negate = ClassFilters.negate(ClassFilters.negate(ClassFilter.TRUE)); + assertThat(negate.matches(String.class)).isTrue(); + assertThat(negate.matches(Object.class)).isTrue(); + assertThat(negate.matches(Integer.class)).isTrue(); + } + + @Test + void negateIsNotEqualsToOriginalFilter() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter negate = ClassFilters.negate(original); + assertThat(original).isNotEqualTo(negate); + } + + @Test + void negateOnSameFilterIsEquals() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter first = ClassFilters.negate(original); + ClassFilter second = ClassFilters.negate(original); + assertThat(first).isEqualTo(second); + } + + @Test + void negateHasNotSameHashCodeAsOriginalFilter() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter negate = ClassFilters.negate(original); + assertThat(original).doesNotHaveSameHashCodeAs(negate); + } + + @Test + void negateOnSameFilterHasSameHashCode() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter first = ClassFilters.negate(original); + ClassFilter second = ClassFilters.negate(original); + assertThat(first).hasSameHashCodeAs(second); + } + + @Test + void toStringIncludesRepresentationOfOriginalFilter() { + ClassFilter original = ClassFilter.TRUE; + assertThat(ClassFilters.negate(original)).hasToString("Negate " + original); + } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java index 1306d6f67555..54b3657703f1 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class ComposablePointcutTests { +class ComposablePointcutTests { public static MethodMatcher GETTER_METHOD_MATCHER = new StaticMethodMatcher() { @Override @@ -56,23 +56,16 @@ public boolean matches(Method m, @Nullable Class targetClass) { } }; - public static MethodMatcher SETTER_METHOD_MATCHER = new StaticMethodMatcher() { - @Override - public boolean matches(Method m, @Nullable Class targetClass) { - return m.getName().startsWith("set"); - } - }; - @Test - public void testMatchAll() throws NoSuchMethodException { + void testMatchAll() throws NoSuchMethodException { Pointcut pc = new ComposablePointcut(); assertThat(pc.getClassFilter().matches(Object.class)).isTrue(); assertThat(pc.getMethodMatcher().matches(Object.class.getMethod("hashCode"), Exception.class)).isTrue(); } @Test - public void testFilterByClass() throws NoSuchMethodException { + void testFilterByClass() { ComposablePointcut pc = new ComposablePointcut(); assertThat(pc.getClassFilter().matches(Object.class)).isTrue(); @@ -92,7 +85,7 @@ public void testFilterByClass() throws NoSuchMethodException { } @Test - public void testUnionMethodMatcher() { + void testUnionMethodMatcher() { // Matches the getAge() method in any class ComposablePointcut pc = new ComposablePointcut(ClassFilter.TRUE, GET_AGE_METHOD_MATCHER); assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); @@ -115,7 +108,7 @@ public void testUnionMethodMatcher() { } @Test - public void testIntersectionMethodMatcher() { + void testIntersectionMethodMatcher() { ComposablePointcut pc = new ComposablePointcut(); assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); @@ -132,7 +125,7 @@ public void testIntersectionMethodMatcher() { } @Test - public void testEqualsAndHashCode() throws Exception { + void testEqualsAndHashCode() { ComposablePointcut pc1 = new ComposablePointcut(); ComposablePointcut pc2 = new ComposablePointcut(); @@ -141,7 +134,7 @@ public void testEqualsAndHashCode() throws Exception { pc1.intersection(GETTER_METHOD_MATCHER); - assertThat(pc1.equals(pc2)).isFalse(); + assertThat(pc1).isNotEqualTo(pc2); assertThat(pc1.hashCode()).isNotEqualTo(pc2.hashCode()); pc2.intersection(GETTER_METHOD_MATCHER); diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java index 3f4bd17c7903..65c08b8725f5 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.aop.support; +import java.lang.reflect.Method; +import java.util.List; +import java.util.regex.Pattern; + import org.junit.jupiter.api.Test; import org.springframework.aop.Pointcut; @@ -27,63 +31,97 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link ControlFlowPointcut}. + * * @author Rod Johnson * @author Chris Beams + * @author Sam Brannen */ -public class ControlFlowPointcutTests { +class ControlFlowPointcutTests { @Test - public void testMatches() { - TestBean target = new TestBean(); - target.setAge(27); + void matchesExactMethodName() { + MyComponent component = new MyComponent(); + TestBean target = new TestBean("Jane", 27); + ControlFlowPointcut cflow = pointcut("getAge"); NopInterceptor nop = new NopInterceptor(); - ControlFlowPointcut cflow = new ControlFlowPointcut(One.class, "getAge"); ProxyFactory pf = new ProxyFactory(target); - ITestBean proxied = (ITestBean) pf.getProxy(); pf.addAdvisor(new DefaultPointcutAdvisor(cflow, nop)); + ITestBean proxy = (ITestBean) pf.getProxy(); - // Not advised, not under One - assertThat(proxied.getAge()).isEqualTo(target.getAge()); + // Will not be advised: not under MyComponent + assertThat(proxy.getAge()).isEqualTo(target.getAge()); + assertThat(cflow.getEvaluations()).isEqualTo(1); assertThat(nop.getCount()).isEqualTo(0); - // Will be advised - assertThat(new One().getAge(proxied)).isEqualTo(target.getAge()); + // Will be advised due to "getAge" pattern: the proxy is invoked under MyComponent#getAge + assertThat(component.getAge(proxy)).isEqualTo(target.getAge()); + assertThat(cflow.getEvaluations()).isEqualTo(2); assertThat(nop.getCount()).isEqualTo(1); - // Won't be advised - assertThat(new One().nomatch(proxied)).isEqualTo(target.getAge()); - assertThat(nop.getCount()).isEqualTo(1); + // Will not be advised: the proxy is invoked under MyComponent, but there is no match for "nomatch" + assertThat(component.nomatch(proxy)).isEqualTo(target.getAge()); assertThat(cflow.getEvaluations()).isEqualTo(3); + assertThat(nop.getCount()).isEqualTo(1); + } + + @Test + void matchesMethodNamePatterns() { + ControlFlowPointcut cflow = pointcut("set", "getAge"); + assertMatchesSetAndGetAge(cflow); + + cflow = pointcut("foo", "get*", "bar", "*se*", "baz"); + assertMatchesSetAndGetAge(cflow); + } + + @Test + void regExControlFlowPointcut() { + ControlFlowPointcut cflow = new RegExControlFlowPointcut(MyComponent.class, "(set.*?|getAge)"); + assertMatchesSetAndGetAge(cflow); + + cflow = new RegExControlFlowPointcut(MyComponent.class, "set", "^getAge$"); + assertMatchesSetAndGetAge(cflow); + } + + @Test + void controlFlowPointcutIsExtensible() { + CustomControlFlowPointcut cflow = new CustomControlFlowPointcut(MyComponent.class, "set*", "getAge", "set*", "set*"); + assertMatchesSetAndGetAge(cflow, 2); + assertThat(cflow.trackedClass()).isEqualTo(MyComponent.class); + assertThat(cflow.trackedMethodNamePatterns()).containsExactly("set*", "getAge"); } /** - * Check that we can use a cflow pointcut only in conjunction with + * Check that we can use a cflow pointcut in conjunction with * a static pointcut: e.g. 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. */ @Test - public void testSelectiveApplication() { - TestBean target = new TestBean(); - target.setAge(27); + void controlFlowPointcutCanBeCombinedWithStaticPointcut() { + MyComponent component = new MyComponent(); + TestBean target = new TestBean("Jane", 27); + ControlFlowPointcut cflow = pointcut(); + Pointcut settersUnderMyComponent = Pointcuts.intersection(Pointcuts.SETTERS, cflow); NopInterceptor nop = new NopInterceptor(); - ControlFlowPointcut cflow = new ControlFlowPointcut(One.class); - Pointcut settersUnderOne = Pointcuts.intersection(Pointcuts.SETTERS, cflow); ProxyFactory pf = new ProxyFactory(target); - ITestBean proxied = (ITestBean) pf.getProxy(); - pf.addAdvisor(new DefaultPointcutAdvisor(settersUnderOne, nop)); + pf.addAdvisor(new DefaultPointcutAdvisor(settersUnderMyComponent, nop)); + ITestBean proxy = (ITestBean) pf.getProxy(); - // Not advised, not under One + // Will not be advised: not under MyComponent target.setAge(16); + assertThat(cflow.getEvaluations()).isEqualTo(0); assertThat(nop.getCount()).isEqualTo(0); - // Not advised; under One but not a setter - assertThat(new One().getAge(proxied)).isEqualTo(16); + // Will not be advised: under MyComponent but not a setter + assertThat(component.getAge(proxy)).isEqualTo(16); + assertThat(cflow.getEvaluations()).isEqualTo(0); assertThat(nop.getCount()).isEqualTo(0); - // Won't be advised - new One().set(proxied); + // Will be advised due to Pointcuts.SETTERS: the proxy is invoked under MyComponent#set + component.set(proxy); + assertThat(proxy.getAge()).isEqualTo(5); assertThat(nop.getCount()).isEqualTo(1); // We saved most evaluations @@ -91,32 +129,149 @@ public void testSelectiveApplication() { } @Test - public void testEqualsAndHashCode() throws Exception { - assertThat(new ControlFlowPointcut(One.class)).isEqualTo(new ControlFlowPointcut(One.class)); - assertThat(new ControlFlowPointcut(One.class, "getAge")).isEqualTo(new ControlFlowPointcut(One.class, "getAge")); - assertThat(new ControlFlowPointcut(One.class, "getAge").equals(new ControlFlowPointcut(One.class))).isFalse(); - assertThat(new ControlFlowPointcut(One.class).hashCode()).isEqualTo(new ControlFlowPointcut(One.class).hashCode()); - assertThat(new ControlFlowPointcut(One.class, "getAge").hashCode()).isEqualTo(new ControlFlowPointcut(One.class, "getAge").hashCode()); - assertThat(new ControlFlowPointcut(One.class, "getAge").hashCode()).isNotEqualTo(new ControlFlowPointcut(One.class).hashCode()); + void equalsAndHashCode() { + assertThat(pointcut()).isEqualTo(pointcut()); + assertThat(pointcut()).hasSameHashCodeAs(pointcut()); + + assertThat(pointcut("getAge")).isEqualTo(pointcut("getAge")); + assertThat(pointcut("getAge")).hasSameHashCodeAs(pointcut("getAge")); + + assertThat(pointcut("getAge")).isNotEqualTo(pointcut()); + assertThat(pointcut("getAge")).doesNotHaveSameHashCodeAs(pointcut()); + + assertThat(pointcut("get*", "set*")).isEqualTo(pointcut("get*", "set*")); + assertThat(pointcut("get*", "set*")).isEqualTo(pointcut("get*", "set*", "set*", "get*")); + assertThat(pointcut("get*", "set*")).hasSameHashCodeAs(pointcut("get*", "get*", "set*")); + + assertThat(pointcut("get*", "set*")).isNotEqualTo(pointcut("set*", "get*")); + assertThat(pointcut("get*", "set*")).doesNotHaveSameHashCodeAs(pointcut("set*", "get*")); + + assertThat(pointcut("get*", "set*")).isEqualTo(pointcut(List.of("get*", "set*"))); + assertThat(pointcut("get*", "set*")).isEqualTo(pointcut(List.of("get*", "set*", "set*", "get*"))); + assertThat(pointcut("get*", "set*")).hasSameHashCodeAs(pointcut(List.of("get*", "get*", "set*"))); } @Test - public void testToString() { - assertThat(new ControlFlowPointcut(One.class).toString()) - .isEqualTo(ControlFlowPointcut.class.getName() + ": class = " + One.class.getName() + "; methodName = null"); - assertThat(new ControlFlowPointcut(One.class, "getAge").toString()) - .isEqualTo(ControlFlowPointcut.class.getName() + ": class = " + One.class.getName() + "; methodName = getAge"); + void testToString() { + String pointcutType = ControlFlowPointcut.class.getName(); + String componentType = MyComponent.class.getName(); + + assertThat(pointcut()).asString() + .startsWith(pointcutType) + .contains(componentType) + .endsWith("[]"); + + assertThat(pointcut("getAge")).asString() + .startsWith(pointcutType) + .contains(componentType) + .endsWith("[getAge]"); + + assertThat(pointcut("get*", "set*", "get*")).asString() + .startsWith(pointcutType) + .contains(componentType) + .endsWith("[get*, set*]"); + } + + + private static ControlFlowPointcut pointcut() { + return new ControlFlowPointcut(MyComponent.class); + } + + private static ControlFlowPointcut pointcut(String methodNamePattern) { + return new ControlFlowPointcut(MyComponent.class, methodNamePattern); + } + + private static ControlFlowPointcut pointcut(String... methodNamePatterns) { + return new ControlFlowPointcut(MyComponent.class, methodNamePatterns); + } + + private static ControlFlowPointcut pointcut(List methodNamePatterns) { + return new ControlFlowPointcut(MyComponent.class, methodNamePatterns); + } + + private static void assertMatchesSetAndGetAge(ControlFlowPointcut cflow) { + assertMatchesSetAndGetAge(cflow, 1); + } + + private static void assertMatchesSetAndGetAge(ControlFlowPointcut cflow, int evaluationFactor) { + MyComponent component = new MyComponent(); + TestBean target = new TestBean("Jane", 27); + NopInterceptor nop = new NopInterceptor(); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvisor(new DefaultPointcutAdvisor(cflow, nop)); + ITestBean proxy = (ITestBean) pf.getProxy(); + + // Will not be advised: not under MyComponent + assertThat(proxy.getAge()).isEqualTo(target.getAge()); + assertThat(cflow.getEvaluations()).isEqualTo(evaluationFactor); + assertThat(nop.getCount()).isEqualTo(0); + + // Will be advised: the proxy is invoked under MyComponent#getAge + assertThat(component.getAge(proxy)).isEqualTo(target.getAge()); + assertThat(cflow.getEvaluations()).isEqualTo(2 * evaluationFactor); + assertThat(nop.getCount()).isEqualTo(1); + + // Will be advised: the proxy is invoked under MyComponent#set + component.set(proxy); + assertThat(cflow.getEvaluations()).isEqualTo(3 * evaluationFactor); + assertThat(proxy.getAge()).isEqualTo(5); + assertThat(cflow.getEvaluations()).isEqualTo(4 * evaluationFactor); + assertThat(nop.getCount()).isEqualTo(2); + + // Will not be advised: the proxy is invoked under MyComponent, but there is no match for "nomatch" + assertThat(component.nomatch(proxy)).isEqualTo(target.getAge()); + assertThat(nop.getCount()).isEqualTo(2); + assertThat(cflow.getEvaluations()).isEqualTo(5 * evaluationFactor); + } + + + private static class MyComponent { + int getAge(ITestBean proxy) { + return proxy.getAge(); + } + int nomatch(ITestBean proxy) { + return proxy.getAge(); + } + void set(ITestBean proxy) { + proxy.setAge(5); + } } - public class One { - int getAge(ITestBean proxied) { - return proxied.getAge(); + @SuppressWarnings("serial") + private static class CustomControlFlowPointcut extends ControlFlowPointcut { + + CustomControlFlowPointcut(Class clazz, String... methodNamePatterns) { + super(clazz, methodNamePatterns); + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + super.incrementEvaluationCount(); + return super.matches(method, targetClass, args); + } + + Class trackedClass() { + return super.clazz; + } + + List trackedMethodNamePatterns() { + return super.methodNamePatterns; } - int nomatch(ITestBean proxied) { - return proxied.getAge(); + } + + @SuppressWarnings("serial") + private static class RegExControlFlowPointcut extends ControlFlowPointcut { + + private final List compiledPatterns; + + RegExControlFlowPointcut(Class clazz, String... methodNamePatterns) { + super(clazz, methodNamePatterns); + this.compiledPatterns = super.methodNamePatterns.stream().map(Pattern::compile).toList(); } - void set(ITestBean proxied) { - proxied.setAge(5); + + @Override + protected boolean isMatch(String methodName, int patternIndex) { + return this.compiledPatterns.get(patternIndex).matcher(methodName).matches(); } } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java index 86128dd4ca63..de5a55463999 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,14 +47,14 @@ class DelegatingIntroductionInterceptorTests { @Test - void testNullTarget() throws Exception { + void testNullTarget() { // Shouldn't accept null target assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingIntroductionInterceptor(null)); } @Test - void testIntroductionInterceptorWithDelegation() throws Exception { + void testIntroductionInterceptorWithDelegation() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -70,7 +70,7 @@ void testIntroductionInterceptorWithDelegation() throws Exception { } @Test - void testIntroductionInterceptorWithInterfaceHierarchy() throws Exception { + void testIntroductionInterceptorWithInterfaceHierarchy() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(SubTimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -86,7 +86,7 @@ void testIntroductionInterceptorWithInterfaceHierarchy() throws Exception { } @Test - void testIntroductionInterceptorWithSuperInterface() throws Exception { + void testIntroductionInterceptorWithSuperInterface() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -107,7 +107,7 @@ void testAutomaticInterfaceRecognitionInDelegate() throws Exception { final long t = 1001L; class Tester implements TimeStamped, ITester { @Override - public void foo() throws Exception { + public void foo() { } @Override public long getTimeStamp() { @@ -138,7 +138,7 @@ void testAutomaticInterfaceRecognitionInSubclass() throws Exception { @SuppressWarnings("serial") class TestII extends DelegatingIntroductionInterceptor implements TimeStamped, ITester { @Override - public void foo() throws Exception { + public void foo() { } @Override public long getTimeStamp() { @@ -177,9 +177,8 @@ public long getTimeStamp() { assertThat(o).isNotInstanceOf(TimeStamped.class); } - @SuppressWarnings("serial") @Test - void testIntroductionInterceptorDoesntReplaceToString() throws Exception { + void testIntroductionInterceptorDoesNotReplaceToString() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -246,7 +245,7 @@ void testSerializableDelegatingIntroductionInterceptorSerializable() throws Exce // Test when target implements the interface: should get interceptor by preference. @Test - void testIntroductionMasksTargetImplementation() throws Exception { + void testIntroductionMasksTargetImplementation() { final long t = 1001L; @SuppressWarnings("serial") class TestII extends DelegatingIntroductionInterceptor implements TimeStamped { diff --git a/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java index 55a2d7cabb57..6fae987c9ea7 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-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,12 +28,15 @@ import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Juergen Hoeller * @author Chris Beams */ -public class MethodMatchersTests { +class MethodMatchersTests { + + private static final Method TEST_METHOD = mock(Method.class); private final Method EXCEPTION_GETMESSAGE; @@ -53,19 +56,19 @@ public MethodMatchersTests() throws Exception { @Test - public void testDefaultMatchesAll() throws Exception { + void testDefaultMatchesAll() { MethodMatcher defaultMm = MethodMatcher.TRUE; assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); } @Test - public void testMethodMatcherTrueSerializable() throws Exception { + void testMethodMatcherTrueSerializable() throws Exception { assertThat(MethodMatcher.TRUE).isSameAs(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)); } @Test - public void testSingle() throws Exception { + void testSingle() { MethodMatcher defaultMm = MethodMatcher.TRUE; assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); @@ -77,7 +80,7 @@ public void testSingle() throws Exception { @Test - public void testDynamicAndStaticMethodMatcherIntersection() throws Exception { + void testDynamicAndStaticMethodMatcherIntersection() { MethodMatcher mm1 = MethodMatcher.TRUE; MethodMatcher mm2 = new TestDynamicMethodMatcherWhichMatches(); MethodMatcher intersection = MethodMatchers.intersection(mm1, mm2); @@ -92,7 +95,7 @@ public void testDynamicAndStaticMethodMatcherIntersection() throws Exception { } @Test - public void testStaticMethodMatcherUnion() throws Exception { + void testStaticMethodMatcherUnion() { MethodMatcher getterMatcher = new StartsWithMatcher("get"); MethodMatcher setterMatcher = new StartsWithMatcher("set"); MethodMatcher union = MethodMatchers.union(getterMatcher, setterMatcher); @@ -104,11 +107,70 @@ public void testStaticMethodMatcherUnion() throws Exception { } @Test - public void testUnionEquals() { + void testUnionEquals() { MethodMatcher first = MethodMatchers.union(MethodMatcher.TRUE, MethodMatcher.TRUE); MethodMatcher second = new ComposablePointcut(MethodMatcher.TRUE).union(new ComposablePointcut(MethodMatcher.TRUE)).getMethodMatcher(); - assertThat(first.equals(second)).isTrue(); - assertThat(second.equals(first)).isTrue(); + assertThat(first).isEqualTo(second); + assertThat(second).isEqualTo(first); + } + + @Test + void negateMethodMatcher() { + MethodMatcher getterMatcher = new StartsWithMatcher("get"); + MethodMatcher negate = MethodMatchers.negate(getterMatcher); + assertThat(negate.matches(ITESTBEAN_SETAGE, int.class)).isTrue(); + } + + @Test + void negateTrueMethodMatcher() { + MethodMatcher negate = MethodMatchers.negate(MethodMatcher.TRUE); + assertThat(negate.matches(TEST_METHOD, String.class)).isFalse(); + assertThat(negate.matches(TEST_METHOD, Object.class)).isFalse(); + assertThat(negate.matches(TEST_METHOD, Integer.class)).isFalse(); + } + + @Test + void negateTrueMethodMatcherAppliedTwice() { + MethodMatcher negate = MethodMatchers.negate(MethodMatchers.negate(MethodMatcher.TRUE)); + assertThat(negate.matches(TEST_METHOD, String.class)).isTrue(); + assertThat(negate.matches(TEST_METHOD, Object.class)).isTrue(); + assertThat(negate.matches(TEST_METHOD, Integer.class)).isTrue(); + } + + @Test + void negateIsNotEqualsToOriginalMatcher() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher negate = MethodMatchers.negate(original); + assertThat(original).isNotEqualTo(negate); + } + + @Test + void negateOnSameMatcherIsEquals() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher first = MethodMatchers.negate(original); + MethodMatcher second = MethodMatchers.negate(original); + assertThat(first).isEqualTo(second); + } + + @Test + void negateHasNotSameHashCodeAsOriginalMatcher() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher negate = MethodMatchers.negate(original); + assertThat(original).doesNotHaveSameHashCodeAs(negate); + } + + @Test + void negateOnSameMatcherHasSameHashCode() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher first = MethodMatchers.negate(original); + MethodMatcher second = MethodMatchers.negate(original); + assertThat(first).hasSameHashCodeAs(second); + } + + @Test + void toStringIncludesRepresentationOfOriginalMatcher() { + MethodMatcher original = MethodMatcher.TRUE; + assertThat(MethodMatchers.negate(original)).hasToString("Negate " + original); } 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 3000734f1135..de0344a9f0fb 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 @@ -30,34 +30,35 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link NameMatchMethodPointcut}. + * * @author Rod Johnson * @author Chris Beams + * @author Sam Brannen */ -public class NameMatchMethodPointcutTests { +class NameMatchMethodPointcutTests { - protected NameMatchMethodPointcut pc; + private final NameMatchMethodPointcut pc = new NameMatchMethodPointcut(); - protected Person proxied; + private final SerializableNopInterceptor nop = new SerializableNopInterceptor(); - protected SerializableNopInterceptor nop; + private Person personProxy; /** * Create an empty pointcut, populating instance variables. */ @BeforeEach - public void setup() { + void setup() { ProxyFactory pf = new ProxyFactory(new SerializablePerson()); - nop = new SerializableNopInterceptor(); - pc = new NameMatchMethodPointcut(); pf.addAdvisor(new DefaultPointcutAdvisor(pc, nop)); - proxied = (Person) pf.getProxy(); + personProxy = (Person) pf.getProxy(); } @Test - public void testMatchingOnly() { - // Can't do exact matching through isMatch + void isMatch() { + assertThat(pc.isMatch("echo", "echo")).isTrue(); assertThat(pc.isMatch("echo", "ech*")).isTrue(); assertThat(pc.isMatch("setName", "setN*")).isTrue(); assertThat(pc.isMatch("setName", "set*")).isTrue(); @@ -67,73 +68,87 @@ public void testMatchingOnly() { } @Test - public void testEmpty() throws Throwable { + void noMappedMethodNamePatterns() throws Throwable { assertThat(nop.getCount()).isEqualTo(0); - proxied.getName(); - proxied.setName(""); - proxied.echo(null); + personProxy.getName(); + personProxy.setName(""); + personProxy.echo(null); assertThat(nop.getCount()).isEqualTo(0); } - @Test - public void testMatchOneMethod() throws Throwable { + void methodNamePatternsMappedIndividually() throws Throwable { pc.addMethodName("echo"); pc.addMethodName("set*"); + + assertThat(nop.getCount()).isEqualTo(0); + + personProxy.getName(); assertThat(nop.getCount()).isEqualTo(0); - proxied.getName(); - proxied.getName(); + + personProxy.getName(); assertThat(nop.getCount()).isEqualTo(0); - proxied.echo(null); + + personProxy.echo(null); assertThat(nop.getCount()).isEqualTo(1); - proxied.setName(""); + personProxy.setName(""); assertThat(nop.getCount()).isEqualTo(2); - proxied.setAge(25); - assertThat(proxied.getAge()).isEqualTo(25); + + personProxy.setAge(25); assertThat(nop.getCount()).isEqualTo(3); + assertThat(personProxy.getAge()).isEqualTo(25); } @Test - public void testSets() throws Throwable { + void methodNamePatternsMappedAsVarargs() throws Throwable { pc.setMappedNames("set*", "echo"); + assertThat(nop.getCount()).isEqualTo(0); - proxied.getName(); - proxied.setName(""); + + personProxy.getName(); + assertThat(nop.getCount()).isEqualTo(0); + + personProxy.setName(""); assertThat(nop.getCount()).isEqualTo(1); - proxied.echo(null); + + personProxy.echo(null); assertThat(nop.getCount()).isEqualTo(2); } @Test - public void testSerializable() throws Throwable { - testSets(); - // Count is now 2 - Person p2 = SerializationTestUtils.serializeAndDeserialize(proxied); + void serializable() throws Throwable { + methodNamePatternsMappedAsVarargs(); + + Person p2 = SerializationTestUtils.serializeAndDeserialize(personProxy); NopInterceptor nop2 = (NopInterceptor) ((Advised) p2).getAdvisors()[0].getAdvice(); + + // nop.getCount() should still be 2. + assertThat(nop2.getCount()).isEqualTo(2); + p2.getName(); assertThat(nop2.getCount()).isEqualTo(2); + p2.echo(null); assertThat(nop2.getCount()).isEqualTo(3); } @Test - public void testEqualsAndHashCode() { + void equalsAndHashCode() { NameMatchMethodPointcut pc1 = new NameMatchMethodPointcut(); NameMatchMethodPointcut pc2 = new NameMatchMethodPointcut(); - - String foo = "foo"; + String mappedNamePattern = "foo"; assertThat(pc2).isEqualTo(pc1); - assertThat(pc2.hashCode()).isEqualTo(pc1.hashCode()); + assertThat(pc2).hasSameHashCodeAs(pc1); - pc1.setMappedName(foo); - assertThat(pc1.equals(pc2)).isFalse(); - assertThat(pc1.hashCode()).isNotEqualTo(pc2.hashCode()); + pc1.setMappedName(mappedNamePattern); + assertThat(pc1).isNotEqualTo(pc2); + assertThat(pc1).doesNotHaveSameHashCodeAs(pc2); - pc2.setMappedName(foo); + pc2.setMappedName(mappedNamePattern); assertThat(pc2).isEqualTo(pc1); - assertThat(pc2.hashCode()).isEqualTo(pc1.hashCode()); + assertThat(pc2).hasSameHashCodeAs(pc1); } } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java index 9a6f05e10171..6f50a9aecc17 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class PointcutsTests { +class PointcutsTests { public static Method TEST_BEAN_SET_AGE; public static Method TEST_BEAN_GET_AGE; @@ -120,7 +120,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { @Test - public void testTrue() { + void testTrue() { assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); @@ -130,7 +130,7 @@ public void testTrue() { } @Test - public void testMatches() { + void testMatches() { assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); @@ -143,7 +143,7 @@ public void testMatches() { * Should match all setters and getters on any class */ @Test - public void testUnionOfSettersAndGetters() { + void testUnionOfSettersAndGetters() { Pointcut union = Pointcuts.union(allClassGetterPointcut, allClassSetterPointcut); assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); @@ -151,7 +151,7 @@ public void testUnionOfSettersAndGetters() { } @Test - public void testUnionOfSpecificGetters() { + void testUnionOfSpecificGetters() { Pointcut union = Pointcuts.union(allClassGetAgePointcut, allClassGetNamePointcut); assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); @@ -175,7 +175,7 @@ public void testUnionOfSpecificGetters() { * Second one matches all getters in the MyTestBean class. TestBean getters shouldn't pass. */ @Test - public void testUnionOfAllSettersAndSubclassSetters() { + void testUnionOfAllSettersAndSubclassSetters() { assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_SET_AGE, MyTestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); @@ -193,7 +193,7 @@ public void testUnionOfAllSettersAndSubclassSetters() { * it's the union of allClassGetAge and subclass getters */ @Test - public void testIntersectionOfSpecificGettersAndSubclassGetters() { + void testIntersectionOfSpecificGettersAndSubclassGetters() { assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_AGE, MyTestBean.class)).isTrue(); assertThat(Pointcuts.matches(myTestBeanGetterPointcut, TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); @@ -239,7 +239,7 @@ public void testIntersectionOfSpecificGettersAndSubclassGetters() { * The intersection of these two pointcuts leaves nothing. */ @Test - public void testSimpleIntersection() { + void testSimpleIntersection() { Pointcut intersection = Pointcuts.intersection(allClassGetterPointcut, allClassSetterPointcut); assertThat(Pointcuts.matches(intersection, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); diff --git a/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java b/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java index c3b546c36049..df0213da7423 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,14 +36,14 @@ * @author Rod Johnson * @author Chris Beams */ -public class RegexpMethodPointcutAdvisorIntegrationTests { +class RegexpMethodPointcutAdvisorIntegrationTests { private static final Resource CONTEXT = qualifiedResource(RegexpMethodPointcutAdvisorIntegrationTests.class, "context.xml"); @Test - public void testSinglePattern() throws Throwable { + void testSinglePattern() throws Throwable { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); ITestBean advised = (ITestBean) bf.getBean("settersAdvised"); @@ -62,7 +62,7 @@ public void testSinglePattern() throws Throwable { } @Test - public void testMultiplePatterns() throws Throwable { + void testMultiplePatterns() throws Throwable { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); // This is a CGLIB proxy, so we can proxy it to the target class @@ -86,7 +86,7 @@ public void testMultiplePatterns() throws Throwable { } @Test - public void testSerialization() throws Throwable { + void testSerialization() throws Throwable { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); // This is a CGLIB proxy, so we can proxy it to the target class diff --git a/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java b/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java index e09344b35bc8..ad60e60ac646 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link RootClassFilter}. + * Tests for {@link RootClassFilter}. * * @author Sam Brannen * @since 5.1.10 diff --git a/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java index 4ccbc02290b8..1598de413721 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link AnnotationMatchingPointcut}. + * Tests for {@link AnnotationMatchingPointcut}. * * @author Sam Brannen * @since 5.1.10 diff --git a/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java b/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java index 6637815d058e..11e8adb2da4f 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,13 @@ /** * @author Stephane Nicoll */ -public class CommonsPool2TargetSourceProxyTests { +class CommonsPool2TargetSourceProxyTests { private static final Resource CONTEXT = qualifiedResource(CommonsPool2TargetSourceProxyTests.class, "context.xml"); @Test - public void testProxy() throws Exception { + void testProxy() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(CONTEXT); diff --git a/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.java index a396adafdcd4..9676b676a094 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class HotSwappableTargetSourceTests { +class HotSwappableTargetSourceTests { /** Initial count value set in bean factory XML */ private static final int INITIAL_COUNT = 10; @@ -68,7 +68,7 @@ public void close() { * Check it works like a normal invoker */ @Test - public void testBasicFunctionality() { + void testBasicFunctionality() { SideEffectBean proxied = (SideEffectBean) beanFactory.getBean("swappable"); assertThat(proxied.getCount()).isEqualTo(INITIAL_COUNT); proxied.doWork(); @@ -80,7 +80,7 @@ public void testBasicFunctionality() { } @Test - public void testValidSwaps() { + void testValidSwaps() { SideEffectBean target1 = (SideEffectBean) beanFactory.getBean("target1"); SideEffectBean target2 = (SideEffectBean) beanFactory.getBean("target2"); @@ -107,7 +107,7 @@ public void testValidSwaps() { } @Test - public void testRejectsSwapToNull() { + void testRejectsSwapToNull() { HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); assertThatIllegalArgumentException().as("Shouldn't be able to swap to invalid value").isThrownBy(() -> swapper.swap(null)) @@ -117,7 +117,7 @@ public void testRejectsSwapToNull() { } @Test - public void testSerialization() throws Exception { + void testSerialization() throws Exception { SerializablePerson sp1 = new SerializablePerson(); sp1.setName("Tony"); SerializablePerson sp2 = new SerializablePerson(); diff --git a/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java index 4551245335ef..266cfedf5093 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class LazyCreationTargetSourceTests { +class LazyCreationTargetSourceTests { @Test - public void testCreateLazy() { + void testCreateLazy() { TargetSource targetSource = new AbstractLazyCreationTargetSource() { @Override protected Object createObject() { diff --git a/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java index 6c38a0f04fc2..84be7d1a5323 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ * @author Chris Beams * @since 07.01.2005 */ -public class LazyInitTargetSourceTests { +class LazyInitTargetSourceTests { private static final Class CLASS = LazyInitTargetSourceTests.class; @@ -42,9 +42,11 @@ public class LazyInitTargetSourceTests { private static final Resource CUSTOM_TARGET_CONTEXT = qualifiedResource(CLASS, "customTarget.xml"); private static final Resource FACTORY_BEAN_CONTEXT = qualifiedResource(CLASS, "factoryBean.xml"); + private final DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + @Test - public void testLazyInitSingletonTargetSource() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void lazyInitSingletonTargetSource() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions(SINGLETON_CONTEXT); bf.preInstantiateSingletons(); @@ -55,8 +57,7 @@ public void testLazyInitSingletonTargetSource() { } @Test - public void testCustomLazyInitSingletonTargetSource() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void customLazyInitSingletonTargetSource() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CUSTOM_TARGET_CONTEXT); bf.preInstantiateSingletons(); @@ -67,25 +68,25 @@ public void testCustomLazyInitSingletonTargetSource() { } @Test - public void testLazyInitFactoryBeanTargetSource() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + @SuppressWarnings("unchecked") + void lazyInitFactoryBeanTargetSource() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions(FACTORY_BEAN_CONTEXT); bf.preInstantiateSingletons(); - Set set1 = (Set) bf.getBean("proxy1"); + Set set1 = (Set) bf.getBean("proxy1"); assertThat(bf.containsSingleton("target1")).isFalse(); - assertThat(set1.contains("10")).isTrue(); + assertThat(set1).contains("10"); assertThat(bf.containsSingleton("target1")).isTrue(); - Set set2 = (Set) bf.getBean("proxy2"); + Set set2 = (Set) bf.getBean("proxy2"); assertThat(bf.containsSingleton("target2")).isFalse(); - assertThat(set2.contains("20")).isTrue(); + assertThat(set2).contains("20"); assertThat(bf.containsSingleton("target2")).isTrue(); } @SuppressWarnings("serial") - public static class CustomLazyInitTargetSource extends LazyInitTargetSource { + static class CustomLazyInitTargetSource extends LazyInitTargetSource { @Override protected void postProcessTargetObject(Object targetObject) { diff --git a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java index 6e857ffe6b18..6846f70962ce 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,10 +36,10 @@ * @author Rod Johnson * @author Chris Beams */ -public class PrototypeBasedTargetSourceTests { +class PrototypeBasedTargetSourceTests { @Test - public void testSerializability() throws Exception { + void testSerializability() throws Exception { MutablePropertyValues tsPvs = new MutablePropertyValues(); tsPvs.add("targetBeanName", "person"); RootBeanDefinition tsBd = new RootBeanDefinition(TestTargetSource.class); @@ -56,10 +56,8 @@ public void testSerializability() throws Exception { TestTargetSource cpts = (TestTargetSource) bf.getBean("ts"); TargetSource serialized = SerializationTestUtils.serializeAndDeserialize(cpts); - boolean condition = serialized instanceof SingletonTargetSource; - assertThat(condition).as("Changed to SingletonTargetSource on deserialization").isTrue(); - SingletonTargetSource sts = (SingletonTargetSource) serialized; - assertThat(sts.getTarget()).isNotNull(); + assertThat(serialized).isInstanceOfSatisfying(SingletonTargetSource.class, + sts -> assertThat(sts.getTarget()).isNotNull()); } @@ -72,17 +70,12 @@ private static class TestTargetSource extends AbstractPrototypeBasedTargetSource * state can't prevent serialization from working */ @SuppressWarnings({"unused", "serial"}) - private TestBean thisFieldIsNotSerializable = new TestBean(); + private final TestBean thisFieldIsNotSerializable = new TestBean(); @Override - public Object getTarget() throws Exception { + public Object getTarget() { return newPrototypeInstance(); } - - @Override - public void releaseTarget(Object target) throws Exception { - // Do nothing - } } } diff --git a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java index 7871e1072d18..ba12b5f6beef 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class PrototypeTargetSourceTests { +class PrototypeTargetSourceTests { /** Initial count value set in bean factory XML */ private static final int INITIAL_COUNT = 10; @@ -52,7 +52,7 @@ public void setup() { * With the singleton, there will be change. */ @Test - public void testPrototypeAndSingletonBehaveDifferently() { + void testPrototypeAndSingletonBehaveDifferently() { SideEffectBean singleton = (SideEffectBean) beanFactory.getBean("singleton"); assertThat(singleton.getCount()).isEqualTo(INITIAL_COUNT); singleton.doWork(); diff --git a/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java index 7cb788ed1ac8..0c227ecd4be4 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class ThreadLocalTargetSourceTests { +class ThreadLocalTargetSourceTests { /** Initial count value set in bean factory XML */ private static final int INITIAL_COUNT = 10; @@ -60,7 +60,7 @@ protected void close() { * with one another. */ @Test - public void testUseDifferentManagedInstancesInSameThread() { + void testUseDifferentManagedInstancesInSameThread() { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -72,7 +72,7 @@ public void testUseDifferentManagedInstancesInSameThread() { } @Test - public void testReuseInSameThread() { + void testReuseInSameThread() { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -86,7 +86,7 @@ public void testReuseInSameThread() { * Relies on introduction. */ @Test - public void testCanGetStatsViaMixin() { + void testCanGetStatsViaMixin() { ThreadLocalTargetSourceStats stats = (ThreadLocalTargetSourceStats) beanFactory.getBean("apartment"); // +1 because creating target for stats call counts assertThat(stats.getInvocationCount()).isEqualTo(1); @@ -104,7 +104,7 @@ public void testCanGetStatsViaMixin() { } @Test - public void testNewThreadHasOwnInstance() throws InterruptedException { + void testNewThreadHasOwnInstance() throws InterruptedException { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -144,7 +144,7 @@ public void run() { * Test for SPR-1442. Destroyed target should re-associated with thread and not throw NPE. */ @Test - public void testReuseDestroyedTarget() { + void testReuseDestroyedTarget() { ThreadLocalTargetSource source = (ThreadLocalTargetSource)this.beanFactory.getBean("threadLocalTs"); // try first time diff --git a/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java index 593b2b27c021..3c81244348d7 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,13 +27,13 @@ * @author Rob Harrop * @author Chris Beams */ -public class RefreshableTargetSourceTests { +class RefreshableTargetSourceTests { /** * Test what happens when checking for refresh but not refreshing object. */ @Test - public void testRefreshCheckWithNonRefresh() throws Exception { + void testRefreshCheckWithNonRefresh() throws Exception { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(); ts.setRefreshCheckDelay(0); @@ -49,7 +49,7 @@ public void testRefreshCheckWithNonRefresh() throws Exception { * Test what happens when checking for refresh and refresh occurs. */ @Test - public void testRefreshCheckWithRefresh() throws Exception { + void testRefreshCheckWithRefresh() throws Exception { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(true); ts.setRefreshCheckDelay(0); @@ -65,7 +65,7 @@ public void testRefreshCheckWithRefresh() throws Exception { * Test what happens when no refresh occurs. */ @Test - public void testWithNoRefreshCheck() throws Exception { + void testWithNoRefreshCheck() { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(true); ts.setRefreshCheckDelay(-1); diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt new file mode 100644 index 000000000000..6e079a70276e --- /dev/null +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.framework + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import kotlin.coroutines.Continuation + +/** + * Tests for [CoroutinesUtils]. + * + * @author Sebastien Deleuze + */ +class CoroutinesUtilsTests { + + @Test + fun awaitSingleNonNullValue() { + val value = "foo" + val continuation = Continuation(CoroutineName("test")) { } + runBlocking { + assertThat(CoroutinesUtils.awaitSingleOrNull(value, continuation)).isEqualTo(value) + } + } + + @Test + fun awaitSingleNullValue() { + val value = null + val continuation = Continuation(CoroutineName("test")) { } + runBlocking { + assertThat(CoroutinesUtils.awaitSingleOrNull(value, continuation)).isNull() + } + } + + @Test + fun awaitSingleMonoValue() { + val value = "foo" + val continuation = Continuation(CoroutineName("test")) { } + runBlocking { + assertThat(CoroutinesUtils.awaitSingleOrNull(Mono.just(value), continuation)).isEqualTo(value) + } + } + + @Test + @Suppress("UNCHECKED_CAST") + fun flow() { + val value1 = "foo" + val value2 = "bar" + val values = Flux.just(value1, value2) + val flow = CoroutinesUtils.asFlow(values) as Flow + runBlocking { + assertThat(flow.toList()).containsExactly(value1, value2) + } + } + +} diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt new file mode 100644 index 000000000000..9d28fca0f7d5 --- /dev/null +++ b/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.support + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.delay +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.util.ReflectionUtils +import reactor.core.publisher.Mono +import kotlin.coroutines.Continuation + +/** + * Tests for Kotlin support in [AopUtils]. + * + * @author Sebastien Deleuze + */ +class AopUtilsKotlinTests { + + @Test + fun `Invoking suspending function should return Mono`() { + val value = "foo" + val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle", + String::class. java, Continuation::class.java)!! + val continuation = Continuation(CoroutineName("test")) { } + val result = AopUtils.invokeJoinpointUsingReflection(WithoutInterface(), method, arrayOf(value, continuation)) + assertThat(result).isInstanceOfSatisfying(Mono::class.java) { + assertThat(it.block()).isEqualTo(value) + } + } + + @Test + fun `Invoking suspending function on bridged method should return Mono`() { + val value = "foo" + val bridgedMethod = ReflectionUtils.findMethod(WithInterface::class.java, "handle", Object::class.java, Continuation::class.java)!! + val continuation = Continuation(CoroutineName("test")) { } + val result = AopUtils.invokeJoinpointUsingReflection(WithInterface(), bridgedMethod, arrayOf(value, continuation)) + assertThat(result).isInstanceOfSatisfying(Mono::class.java) { + assertThat(it.block()).isEqualTo(value) + } + } + + @Suppress("unused") + suspend fun suspendingFunction(value: String): String { + delay(1) + return value + } + + class WithoutInterface { + suspend fun handle(value: String): String { + delay(1) + return value + } + } + + interface ProxyInterface { + suspend fun handle(value: T): T + } + + class WithInterface : ProxyInterface { + override suspend fun handle(value: String): String { + delay(1) + return value + } + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java index 606101af9c62..e718663d4de8 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,9 +34,4 @@ public void afterThrowing(RemoteException ex) throws Throwable { count("remoteException"); } - /** Not valid, wrong number of arguments */ - public void afterThrowing(Method m, Exception ex) throws Throwable { - throw new UnsupportedOperationException("Shouldn't be called"); - } - } diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonExpressions.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonExpressions.java new file mode 100644 index 000000000000..477ceb1173fa --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonExpressions.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.testfixture.aspectj; + +/** + * Common expressions that are used in tests. + * + * @author Stephane Nicoll + */ +public class CommonExpressions { + + /** + * An expression pointcut that matches all methods + */ + public static final String MATCH_ALL_METHODS = "execution(* *(..))"; + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.java index 33ffc13f39f9..6c08f24ce2fc 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,9 +44,6 @@ public void unlock() { this.locked = false; } - /** - * @see test.mixin.AopProxyTests.Lockable#locked() - */ @Override public boolean locked() { return this.locked; @@ -54,10 +51,8 @@ public boolean locked() { /** * Note that we need to override around advice. - * If the method is a setter and we're locked, prevent execution. - * Otherwise let super.invoke() handle it, and do normal - * Lockable(this) then target behaviour. - * @see org.aopalliance.MethodInterceptor#invoke(org.aopalliance.MethodInvocation) + * If the method is a setter, and we're locked, prevent execution. + * Otherwise, let super.invoke() handle it. */ @Override public Object invoke(MethodInvocation invocation) throws Throwable { diff --git a/spring-aspects/spring-aspects.gradle b/spring-aspects/spring-aspects.gradle index f6922f038706..6ca211a8dd6b 100644 --- a/spring-aspects/spring-aspects.gradle +++ b/spring-aspects/spring-aspects.gradle @@ -2,12 +2,6 @@ description = "Spring Aspects" apply plugin: "io.freefair.aspectj" -sourceSets.main.aspectj.srcDir "src/main/java" -sourceSets.main.java.srcDirs = files() - -sourceSets.test.aspectj.srcDir "src/test/java" -sourceSets.test.java.srcDirs = files() - compileAspectj { sourceCompatibility "17" targetCompatibility "17" 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 923e459b4438..0c7472383dde 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-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,10 +85,4 @@ public aspect AnnotationBeanConfigurerAspect extends AbstractInterfaceDrivenDepe declare parents: @Configurable * implements ConfigurableObject; - /* - * This declaration shouldn't be needed, - * except for an AspectJ bug (https://bugs.eclipse.org/bugs/show_bug.cgi?id=214559) - */ - declare parents: @Configurable Serializable+ implements ConfigurableDeserializationSupport; - } diff --git a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java index 43947fa29c29..a37e14101732 100644 --- a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java +++ b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,12 @@ /** * @author Adrian Colyer + * @author Juergen Hoeller */ -public class AutoProxyWithCodeStyleAspectsTests { +class AutoProxyWithCodeStyleAspectsTests { @Test - @SuppressWarnings("resource") - public void noAutoproxyingOfAjcCompiledAspects() { + void noAutoProxyingOfAjcCompiledAspects() { new ClassPathXmlApplicationContext("org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml"); } diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java index a02bcad6793b..f47eb7ba3c53 100644 --- a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,14 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; -public class SpringConfiguredWithAutoProxyingTests { +/** + * @author Ramnivas Laddad + * @author Juergen Hoeller + */ +class SpringConfiguredWithAutoProxyingTests { @Test - @SuppressWarnings("resource") - public void springConfiguredAndAutoProxyUsedTogether() { - // instantiation is sufficient to trigger failure if this is going to fail... + void springConfiguredAndAutoProxyUsedTogether() { new ClassPathXmlApplicationContext("org/springframework/beans/factory/aspectj/springConfigured.xml"); } diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java index 71e98f68295d..053417e21878 100644 --- a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,10 @@ /** * @author Chris Beams */ -public class XmlBeanConfigurerTests { +class XmlBeanConfigurerTests { @Test - public void injection() { + void injection() { try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/beans/factory/aspectj/beanConfigurerTests.xml")) { diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java index b272e70e307b..8cec3f65ea54 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -451,7 +451,7 @@ protected void testMultiCache(CacheableService service) { protected void testMultiEvict(CacheableService service) { Object o1 = new Object(); - Object o2 = o1.toString() + "A"; + Object o2 = o1 + "A"; Object r1 = service.multiCache(o1); @@ -543,7 +543,7 @@ protected void testMultiConditionalCacheAndEvict(CacheableService service) { Object r1 = service.multiConditionalCacheAndEvict(key); Object r3 = service.multiConditionalCacheAndEvict(key); - assertThat(r1.equals(r3)).isFalse(); + assertThat(r1).isNotEqualTo(r3); assertThat(primary.get(key)).isNull(); Object key2 = 3; @@ -556,132 +556,132 @@ protected void testMultiConditionalCacheAndEvict(CacheableService service) { } @Test - public void testCacheable() { + void testCacheable() { testCacheable(this.cs); } @Test - public void testCacheableNull() { + void testCacheableNull() { testCacheableNull(this.cs); } @Test - public void testCacheableSync() { + void testCacheableSync() { testCacheableSync(this.cs); } @Test - public void testCacheableSyncNull() { + void testCacheableSyncNull() { testCacheableSyncNull(this.cs); } @Test - public void testEvict() { + void testEvict() { testEvict(this.cs, true); } @Test - public void testEvictEarly() { + void testEvictEarly() { testEvictEarly(this.cs); } @Test - public void testEvictWithException() { + void testEvictWithException() { testEvictException(this.cs); } @Test - public void testEvictAll() { + void testEvictAll() { testEvictAll(this.cs, true); } @Test - public void testEvictAllEarly() { + void testEvictAllEarly() { testEvictAllEarly(this.cs); } @Test - public void testEvictWithKey() { + void testEvictWithKey() { testEvictWithKey(this.cs); } @Test - public void testEvictWithKeyEarly() { + void testEvictWithKeyEarly() { testEvictWithKeyEarly(this.cs); } @Test - public void testConditionalExpression() { + void testConditionalExpression() { testConditionalExpression(this.cs); } @Test - public void testConditionalExpressionSync() { + void testConditionalExpressionSync() { testConditionalExpressionSync(this.cs); } @Test - public void testUnlessExpression() { + void testUnlessExpression() { testUnlessExpression(this.cs); } @Test - public void testClassCacheUnlessExpression() { + void testClassCacheUnlessExpression() { testUnlessExpression(this.cs); } @Test - public void testKeyExpression() { + void testKeyExpression() { testKeyExpression(this.cs); } @Test - public void testVarArgsKey() { + void testVarArgsKey() { testVarArgsKey(this.cs); } @Test - public void testClassCacheCacheable() { + void testClassCacheCacheable() { testCacheable(this.ccs); } @Test - public void testClassCacheEvict() { + void testClassCacheEvict() { testEvict(this.ccs, true); } @Test - public void testClassEvictEarly() { + void testClassEvictEarly() { testEvictEarly(this.ccs); } @Test - public void testClassEvictAll() { + void testClassEvictAll() { testEvictAll(this.ccs, true); } @Test - public void testClassEvictWithException() { + void testClassEvictWithException() { testEvictException(this.ccs); } @Test - public void testClassCacheEvictWithWKey() { + void testClassCacheEvictWithWKey() { testEvictWithKey(this.ccs); } @Test - public void testClassEvictWithKeyEarly() { + void testClassEvictWithKeyEarly() { testEvictWithKeyEarly(this.ccs); } @Test - public void testNullValue() { + void testNullValue() { testNullValue(this.cs); } @Test - public void testClassNullValue() { + void testClassNullValue() { Object key = new Object(); assertThat(this.ccs.nullValue(key)).isNull(); int nr = this.ccs.nullInvocations().intValue(); @@ -694,27 +694,27 @@ public void testClassNullValue() { } @Test - public void testMethodName() { + void testMethodName() { testMethodName(this.cs, "name"); } @Test - public void testClassMethodName() { + void testClassMethodName() { testMethodName(this.ccs, "nametestCache"); } @Test - public void testRootVars() { + void testRootVars() { testRootVars(this.cs); } @Test - public void testClassRootVars() { + void testClassRootVars() { testRootVars(this.ccs); } @Test - public void testCustomKeyGenerator() { + void testCustomKeyGenerator() { Object param = new Object(); Object r1 = this.cs.customKeyGenerator(param); assertThat(this.cs.customKeyGenerator(param)).isSameAs(r1); @@ -725,14 +725,14 @@ public void testCustomKeyGenerator() { } @Test - public void testUnknownCustomKeyGenerator() { + void testUnknownCustomKeyGenerator() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomKeyGenerator(param)); } @Test - public void testCustomCacheManager() { + void testCustomCacheManager() { CacheManager customCm = this.ctx.getBean("customCacheManager", CacheManager.class); Object key = new Object(); Object r1 = this.cs.customCacheManager(key); @@ -743,139 +743,139 @@ public void testCustomCacheManager() { } @Test - public void testUnknownCustomCacheManager() { + void testUnknownCustomCacheManager() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomCacheManager(param)); } @Test - public void testNullArg() { + void testNullArg() { testNullArg(this.cs); } @Test - public void testClassNullArg() { + void testClassNullArg() { testNullArg(this.ccs); } @Test - public void testCheckedException() { + void testCheckedException() { testCheckedThrowable(this.cs); } @Test - public void testClassCheckedException() { + void testClassCheckedException() { testCheckedThrowable(this.ccs); } @Test - public void testCheckedExceptionSync() { + void testCheckedExceptionSync() { testCheckedThrowableSync(this.cs); } @Test - public void testClassCheckedExceptionSync() { + void testClassCheckedExceptionSync() { testCheckedThrowableSync(this.ccs); } @Test - public void testUncheckedException() { + void testUncheckedException() { testUncheckedThrowable(this.cs); } @Test - public void testClassUncheckedException() { + void testClassUncheckedException() { testUncheckedThrowable(this.ccs); } @Test - public void testUncheckedExceptionSync() { + void testUncheckedExceptionSync() { testUncheckedThrowableSync(this.cs); } @Test - public void testClassUncheckedExceptionSync() { + void testClassUncheckedExceptionSync() { testUncheckedThrowableSync(this.ccs); } @Test - public void testUpdate() { + void testUpdate() { testCacheUpdate(this.cs); } @Test - public void testClassUpdate() { + void testClassUpdate() { testCacheUpdate(this.ccs); } @Test - public void testConditionalUpdate() { + void testConditionalUpdate() { testConditionalCacheUpdate(this.cs); } @Test - public void testClassConditionalUpdate() { + void testClassConditionalUpdate() { testConditionalCacheUpdate(this.ccs); } @Test - public void testMultiCache() { + void testMultiCache() { testMultiCache(this.cs); } @Test - public void testClassMultiCache() { + void testClassMultiCache() { testMultiCache(this.ccs); } @Test - public void testMultiEvict() { + void testMultiEvict() { testMultiEvict(this.cs); } @Test - public void testClassMultiEvict() { + void testClassMultiEvict() { testMultiEvict(this.ccs); } @Test - public void testMultiPut() { + void testMultiPut() { testMultiPut(this.cs); } @Test - public void testClassMultiPut() { + void testClassMultiPut() { testMultiPut(this.ccs); } @Test - public void testPutRefersToResult() { + void testPutRefersToResult() { testPutRefersToResult(this.cs); } @Test - public void testClassPutRefersToResult() { + void testClassPutRefersToResult() { testPutRefersToResult(this.ccs); } @Test - public void testMultiCacheAndEvict() { + void testMultiCacheAndEvict() { testMultiCacheAndEvict(this.cs); } @Test - public void testClassMultiCacheAndEvict() { + void testClassMultiCacheAndEvict() { testMultiCacheAndEvict(this.ccs); } @Test - public void testMultiConditionalCacheAndEvict() { + void testMultiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(this.cs); } @Test - public void testClassMultiConditionalCacheAndEvict() { + void testClassMultiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(this.ccs); } diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java index 4601c0ea814c..ffeab37c4213 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ /** * @author Costin Leau */ -public class AspectJCacheAnnotationTests extends AbstractCacheAnnotationTests { +class AspectJCacheAnnotationTests extends AbstractCacheAnnotationTests { @Override protected ConfigurableApplicationContext getApplicationContext() { @@ -37,7 +37,7 @@ protected ConfigurableApplicationContext getApplicationContext() { } @Test - public void testKeyStrategy() { + void testKeyStrategy() { AnnotationCacheAspect aspect = ctx.getBean( "org.springframework.cache.config.internalCacheAspect", AnnotationCacheAspect.class); assertThat(aspect.getKeyGenerator()).isSameAs(ctx.getBean("keyGenerator")); diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java index 7d73e484b4cf..75647051d42a 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ /** * @author Stephane Nicoll */ -public class AspectJEnableCachingIsolatedTests { +class AspectJEnableCachingIsolatedTests { private ConfigurableApplicationContext ctx; @@ -65,14 +65,14 @@ public void closeContext() { @Test - public void testKeyStrategy() { + void testKeyStrategy() { load(EnableCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); assertThat(aspect.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator", KeyGenerator.class)); } @Test - public void testCacheErrorHandler() { + void testCacheErrorHandler() { load(EnableCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); assertThat(aspect.getErrorHandler()).isSameAs(this.ctx.getBean("errorHandler", CacheErrorHandler.class)); @@ -82,12 +82,12 @@ public void testCacheErrorHandler() { // --- local tests ------- @Test - public void singleCacheManagerBean() { + void singleCacheManagerBean() { load(SingleCacheManagerConfig.class); } @Test - public void multipleCacheManagerBeans() { + void multipleCacheManagerBeans() { try { load(MultiCacheManagerConfig.class); } @@ -97,12 +97,12 @@ public void multipleCacheManagerBeans() { } @Test - public void multipleCacheManagerBeans_implementsCachingConfigurer() { + void multipleCacheManagerBeans_implementsCachingConfigurer() { load(MultiCacheManagerConfigurer.class); // does not throw } @Test - public void multipleCachingConfigurers() { + void multipleCachingConfigurers() { try { load(MultiCacheManagerConfigurer.class, EnableCachingConfig.class); } @@ -112,7 +112,7 @@ public void multipleCachingConfigurers() { } @Test - public void noCacheManagerBeans() { + void noCacheManagerBeans() { try { load(EmptyConfig.class); } @@ -132,7 +132,7 @@ public void emptyConfigSupport() { } @Test - public void bothSetOnlyResolverIsUsed() { + void bothSetOnlyResolverIsUsed() { load(FullCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java index 4c5f7b414ab5..8b3b440782ce 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ /** * @author Stephane Nicoll */ -public class AspectJEnableCachingTests extends AbstractCacheAnnotationTests { +class AspectJEnableCachingTests extends AbstractCacheAnnotationTests { @Override protected ConfigurableApplicationContext getApplicationContext() { diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java index dc278365d727..a106633859c1 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ /** * @author Stephane Nicoll */ -public class JCacheAspectJJavaConfigTests extends AbstractJCacheAnnotationTests { +class JCacheAspectJJavaConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java index c755d8c3f4aa..a2879aa57639 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ * @author Stephane Nicoll * @author Sam Brannen */ -public class JCacheAspectJNamespaceConfigTests extends AbstractJCacheAnnotationTests { +class JCacheAspectJNamespaceConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java index fa494c921280..447c4cc0f617 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -209,7 +209,7 @@ public Object multiCache(Object arg1) { } @Override - @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#a0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) + @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#a0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) public Object multiEvict(Object arg1) { return this.counter.getAndIncrement(); } 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 62d78b7159b9..0219086ed48d 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 @@ -16,6 +16,8 @@ package org.springframework.cache.config; +import java.util.Objects; + import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -42,7 +44,7 @@ public void setId(Long id) { @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.id); + return Objects.hashCode(this.id); } @Override 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 ae781c8fa697..49544d99e051 100644 --- a/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.java +++ b/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,11 +33,11 @@ * @author Chris Beams * @since 3.1 */ -public class AnnotationBeanConfigurerTests { +class AnnotationBeanConfigurerTests { @Test - public void injection() { - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) { + void injection() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) { ShouldBeConfiguredBySpring myObject = new ShouldBeConfiguredBySpring(); assertThat(myObject.getName()).isEqualTo("Rod"); } diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java index 624d96a27709..6cf02e31db2c 100644 --- a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; /** - * Unit tests for {@link AnnotationAsyncExecutionAspect}. + * Tests for {@link AnnotationAsyncExecutionAspect}. * * @author Ramnivas Laddad * @author Stephane Nicoll @@ -64,7 +64,7 @@ public void setUp() { @Test - public void asyncMethodGetsRoutedAsynchronously() { + void asyncMethodGetsRoutedAsynchronously() { ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); obj.incrementAsync(); executor.waitForCompletion(); @@ -74,7 +74,7 @@ public void asyncMethodGetsRoutedAsynchronously() { } @Test - public void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { + void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); Future future = obj.incrementReturningAFuture(); // No need to executor.waitForCompletion() as future.get() will have the same effect @@ -85,7 +85,7 @@ public void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture( } @Test - public void syncMethodGetsRoutedSynchronously() { + void syncMethodGetsRoutedSynchronously() { ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); obj.increment(); assertThat(obj.counter).isEqualTo(1); @@ -94,7 +94,7 @@ public void syncMethodGetsRoutedSynchronously() { } @Test - public void voidMethodInAsyncClassGetsRoutedAsynchronously() { + void voidMethodInAsyncClassGetsRoutedAsynchronously() { ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); obj.increment(); executor.waitForCompletion(); @@ -104,7 +104,7 @@ public void voidMethodInAsyncClassGetsRoutedAsynchronously() { } @Test - public void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { + void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); Future future = obj.incrementReturningAFuture(); assertThat(future.get().intValue()).isEqualTo(5); @@ -115,7 +115,7 @@ public void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsA /* @Test - public void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() { + void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() { ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); int returnValue = obj.return5(); assertEquals(5, returnValue); @@ -125,7 +125,7 @@ public void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() */ @Test - public void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedException, ExecutionException { + void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedException, ExecutionException { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition("e1", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); AnnotationAsyncExecutionAspect.aspectOf().setBeanFactory(beanFactory); @@ -144,7 +144,7 @@ public void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws Interrupted } @Test - public void exceptionHandlerCalled() { + void exceptionHandlerCalled() { Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid"); TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(); AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler); @@ -161,7 +161,7 @@ public void exceptionHandlerCalled() { } @Test - public void exceptionHandlerNeverThrowsUnexpectedException() { + void exceptionHandlerNeverThrowsUnexpectedException() { Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid"); TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(true); AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler); @@ -221,7 +221,7 @@ public void increment() { @Async public Future incrementReturningAFuture() { counter++; - return new AsyncResult(5); + return new AsyncResult<>(5); } /** @@ -256,7 +256,7 @@ public int return5() { public Future incrementReturningAFuture() { counter++; - return new AsyncResult(5); + return new AsyncResult<>(5); } } @@ -265,12 +265,12 @@ static class ClassWithQualifiedAsyncMethods { @Async public Future defaultWork() { - return new AsyncResult(Thread.currentThread()); + return new AsyncResult<>(Thread.currentThread()); } @Async("e1") public ListenableFuture e1Work() { - return new AsyncResult(Thread.currentThread()); + return new AsyncResult<>(Thread.currentThread()); } @Async("e1") diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java index 20c22f4dbb75..ee8799a68f15 100644 --- a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ /** * @author Stephane Nicoll */ -public class AnnotationDrivenBeanDefinitionParserTests { +class AnnotationDrivenBeanDefinitionParserTests { private ConfigurableApplicationContext context; @@ -50,7 +50,7 @@ public void after() { } @Test - public void asyncAspectRegistered() { + void asyncAspectRegistered() { assertThat(context.containsBean(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME)).isTrue(); } diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java index 616e42996080..86d575febd6a 100644 --- a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,14 +74,6 @@ public void await(long timeout) { } } - private static final class UncaughtExceptionDescriptor { - private final Throwable ex; - - private final Method method; - - private UncaughtExceptionDescriptor(Throwable ex, Method method) { - this.ex = ex; - this.method = method; - } + private record UncaughtExceptionDescriptor(Throwable ex, Method method) { } } diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java index b6ef121a7962..0a0280aa8b89 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,14 +47,14 @@ public void setUp() { } @Test - public void commitOnAnnotatedPublicMethod() throws Throwable { + void commitOnAnnotatedPublicMethod() throws Throwable { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationPublicAnnotatedMember().echo(null); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void matchingRollbackOnApplied() throws Throwable { + void matchingRollbackOnApplied() { assertThat(this.txManager.begun).isEqualTo(0); InterruptedException test = new InterruptedException(); assertThatExceptionOfType(InterruptedException.class).isThrownBy(() -> @@ -65,7 +65,7 @@ public void matchingRollbackOnApplied() throws Throwable { } @Test - public void nonMatchingRollbackOnApplied() throws Throwable { + void nonMatchingRollbackOnApplied() { assertThat(this.txManager.begun).isEqualTo(0); IOException test = new IOException(); assertThatIOException().isThrownBy(() -> @@ -76,35 +76,35 @@ public void nonMatchingRollbackOnApplied() throws Throwable { } @Test - public void commitOnAnnotatedProtectedMethod() { + void commitOnAnnotatedProtectedMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationProtectedAnnotatedMember().doInTransaction(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void nonAnnotatedMethodCallingProtectedMethod() { + void nonAnnotatedMethodCallingProtectedMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationProtectedAnnotatedMember().doSomething(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void commitOnAnnotatedPrivateMethod() { + void commitOnAnnotatedPrivateMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationPrivateAnnotatedMember().doInTransaction(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void nonAnnotatedMethodCallingPrivateMethod() { + void nonAnnotatedMethodCallingPrivateMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationPrivateAnnotatedMember().doSomething(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void notTransactional() { + void notTransactional() { assertThat(this.txManager.begun).isEqualTo(0); new TransactionAspectTests.NotTransactional().noop(); assertThat(this.txManager.begun).isEqualTo(0); diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java index 2d92b8a59b79..d706674d7204 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class TransactionAspectTests { +class TransactionAspectTests { private final CallCountingTransactionManager txManager = new CallCountingTransactionManager(); @@ -56,7 +56,7 @@ public void initContext() { @Test - public void testCommitOnAnnotatedClass() throws Throwable { + void testCommitOnAnnotatedClass() throws Throwable { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); annotationOnlyOnClassWithNoInterface.echo(null); @@ -64,7 +64,7 @@ public void testCommitOnAnnotatedClass() throws Throwable { } @Test - public void commitOnAnnotatedProtectedMethod() throws Throwable { + void commitOnAnnotatedProtectedMethod() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); beanWithAnnotatedProtectedMethod.doInTransaction(); @@ -72,7 +72,7 @@ public void commitOnAnnotatedProtectedMethod() throws Throwable { } @Test - public void commitOnAnnotatedPrivateMethod() throws Throwable { + void commitOnAnnotatedPrivateMethod() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); beanWithAnnotatedPrivateMethod.doSomething(); @@ -80,7 +80,7 @@ public void commitOnAnnotatedPrivateMethod() throws Throwable { } @Test - public void commitOnNonAnnotatedNonPublicMethodInTransactionalType() throws Throwable { + void commitOnNonAnnotatedNonPublicMethodInTransactionalType() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); annotationOnlyOnClassWithNoInterface.nonTransactionalMethod(); @@ -88,7 +88,7 @@ public void commitOnNonAnnotatedNonPublicMethodInTransactionalType() throws Thro } @Test - public void commitOnAnnotatedMethod() throws Throwable { + void commitOnAnnotatedMethod() throws Throwable { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); methodAnnotationOnly.echo(null); @@ -96,7 +96,7 @@ public void commitOnAnnotatedMethod() throws Throwable { } @Test - public void notTransactional() throws Throwable { + void notTransactional() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); new NotTransactional().noop(); @@ -104,45 +104,45 @@ public void notTransactional() throws Throwable { } @Test - public void defaultCommitOnAnnotatedClass() throws Throwable { + void defaultCommitOnAnnotatedClass() { Exception ex = new Exception(); assertThatException() - .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), false)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), false)) + .isSameAs(ex); } @Test - public void defaultRollbackOnAnnotatedClass() throws Throwable { + void defaultRollbackOnAnnotatedClass() { RuntimeException ex = new RuntimeException(); assertThatRuntimeException() - .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), true)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), true)) + .isSameAs(ex); } @Test - public void defaultCommitOnSubclassOfAnnotatedClass() throws Throwable { + void defaultCommitOnSubclassOfAnnotatedClass() { Exception ex = new Exception(); assertThatException() - .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalAnnotation().echo(ex), false)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalAnnotation().echo(ex), false)) + .isSameAs(ex); } @Test - public void defaultCommitOnSubclassOfClassWithTransactionalMethodAnnotated() throws Throwable { + void defaultCommitOnSubclassOfClassWithTransactionalMethodAnnotated() { Exception ex = new Exception(); assertThatException() - .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalMethodAnnotation().echo(ex), false)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalMethodAnnotation().echo(ex), false)) + .isSameAs(ex); } @Test - public void noCommitOnImplementationOfAnnotatedInterface() throws Throwable { + void noCommitOnImplementationOfAnnotatedInterface() { Exception ex = new Exception(); testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(ex), ex); } @Test - public void noRollbackOnImplementationOfAnnotatedInterface() throws Throwable { + void noRollbackOnImplementationOfAnnotatedInterface() { Exception rollbackProvokingException = new RuntimeException(); testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(rollbackProvokingException), rollbackProvokingException); @@ -157,19 +157,19 @@ protected void testRollback(TransactionOperationCallback toc, boolean rollback) } finally { assertThat(txManager.begun).isEqualTo(1); - long expected1 = rollback ? 0 : 1; + long expected1 = (rollback ? 0 : 1); assertThat(txManager.commits).isEqualTo(expected1); - long expected = rollback ? 1 : 0; + long expected = (rollback ? 1 : 0); assertThat(txManager.rollbacks).isEqualTo(expected); } } - protected void testNotTransactional(TransactionOperationCallback toc, Throwable expected) throws Throwable { + protected void testNotTransactional(TransactionOperationCallback toc, Throwable expected) { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); assertThatExceptionOfType(Throwable.class) - .isThrownBy(toc::performTransactionalOperation) - .isSameAs(expected); + .isThrownBy(toc::performTransactionalOperation) + .isSameAs(expected); assertThat(txManager.begun).isEqualTo(0); } diff --git a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml index 6be707bf51dd..e6c494c4f966 100644 --- a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml +++ b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml @@ -2,16 +2,29 @@ + http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd + http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache-3.1.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd"> - + + + + + + + - + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml index 54ddbfd44a7b..9366abc646ee 100644 --- a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml +++ b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml @@ -24,8 +24,7 @@ - + diff --git a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml index 61d1d3a8e9bf..2bc3dc1d113d 100644 --- a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml +++ b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml @@ -7,12 +7,10 @@ http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd"> - + - + diff --git a/spring-beans/spring-beans.gradle b/spring-beans/spring-beans.gradle index fdd14304bdbd..c4fb10eb3200 100644 --- a/spring-beans/spring-beans.gradle +++ b/spring-beans/spring-beans.gradle @@ -8,6 +8,7 @@ dependencies { optional("org.apache.groovy:groovy-xml") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") + optional("org.reactivestreams:reactive-streams") optional("org.yaml:snakeyaml") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation("com.google.code.findbugs:jsr305") @@ -15,4 +16,5 @@ dependencies { testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-core"))) testImplementation("jakarta.annotation:jakarta.annotation-api") + testImplementation("javax.inject:javax.inject") } diff --git a/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java index ec828fb185b6..58e6b215bba9 100644 --- a/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java +++ b/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,30 +55,30 @@ public void setup() { RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); switch (this.mode) { - case "simple": - break; - case "dependencyCheck": + case "simple" -> { + } + case "dependencyCheck" -> { rbd = new RootBeanDefinition(LifecycleBean.class); rbd.setDependencyCheck(RootBeanDefinition.DEPENDENCY_CHECK_OBJECTS); this.beanFactory.addBeanPostProcessor(new LifecycleBean.PostProcessor()); - break; - case "constructor": + } + case "constructor" -> { rbd.getConstructorArgumentValues().addGenericArgumentValue("juergen"); rbd.getConstructorArgumentValues().addGenericArgumentValue("99"); - break; - case "constructorArgument": + } + case "constructorArgument" -> { rbd.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("spouse")); this.beanFactory.registerBeanDefinition("test", rbd); this.beanFactory.registerBeanDefinition("spouse", new RootBeanDefinition(TestBean.class)); - break; - case "properties": + } + case "properties" -> { rbd.getPropertyValues().add("name", "juergen"); rbd.getPropertyValues().add("age", "99"); - break; - case "resolvedProperties": + } + case "resolvedProperties" -> { rbd.getPropertyValues().add("spouse", new RuntimeBeanReference("spouse")); this.beanFactory.registerBeanDefinition("spouse", new RootBeanDefinition(TestBean.class)); - break; + } } rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); this.beanFactory.registerBeanDefinition("test", rbd); diff --git a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt index 9bbdd9711df0..44fd7e654c0e 100644 --- a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt +++ b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.beans -import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeUnit import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode @@ -30,22 +30,22 @@ import org.openjdk.jmh.annotations.State @OutputTimeUnit(TimeUnit.NANOSECONDS) open class KotlinBeanUtilsBenchmark { - private val noArgConstructor = TestClass1::class.java.getDeclaredConstructor() - private val constructor = TestClass2::class.java.getDeclaredConstructor(Int::class.java, String::class.java) + private val noArgConstructor = TestClass1::class.java.getDeclaredConstructor() + private val constructor = TestClass2::class.java.getDeclaredConstructor(Int::class.java, String::class.java) - @Benchmark - fun emptyConstructor(): Any { + @Benchmark + fun emptyConstructor(): Any { return BeanUtils.instantiateClass(noArgConstructor) - } + } - @Benchmark - fun nonEmptyConstructor(): Any { + @Benchmark + fun nonEmptyConstructor(): Any { return BeanUtils.instantiateClass(constructor, 1, "str") - } + } - class TestClass1() + class TestClass1 - @Suppress("UNUSED_PARAMETER") - class TestClass2(int: Int, string: String) + @Suppress("UNUSED_PARAMETER") + class TestClass2(int: Int, string: String) } diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index bee8f5e95ba6..04fc76399ad1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -292,7 +291,7 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) String lastKey = tokens.keys[tokens.keys.length - 1]; if (propValue.getClass().isArray()) { - Class requiredType = propValue.getClass().getComponentType(); + Class requiredType = propValue.getClass().componentType(); int arrayIndex = Integer.parseInt(lastKey); Object oldValue = null; try { @@ -303,7 +302,7 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) requiredType, ph.nested(tokens.keys.length)); int length = Array.getLength(propValue); if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { - Class componentType = propValue.getClass().getComponentType(); + Class componentType = propValue.getClass().componentType(); Object newArray = Array.newInstance(componentType, arrayIndex + 1); System.arraycopy(propValue, 0, newArray, 0, length); int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); @@ -320,14 +319,14 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) } else if (propValue instanceof List list) { - Class requiredType = ph.getCollectionType(tokens.keys.length); + TypeDescriptor requiredType = ph.getCollectionType(tokens.keys.length); int index = Integer.parseInt(lastKey); Object oldValue = null; if (isExtractOldValueForEditor() && index < list.size()) { oldValue = list.get(index); } Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), - requiredType, ph.nested(tokens.keys.length)); + requiredType.getResolvableType().resolve(), requiredType); int size = list.size(); if (index >= size && index < this.autoGrowCollectionLimit) { for (int i = size; i < index; i++) { @@ -355,12 +354,12 @@ else if (propValue instanceof List list) { } else if (propValue instanceof Map map) { - Class mapKeyType = ph.getMapKeyType(tokens.keys.length); - Class mapValueType = ph.getMapValueType(tokens.keys.length); + TypeDescriptor mapKeyType = ph.getMapKeyType(tokens.keys.length); + TypeDescriptor mapValueType = ph.getMapValueType(tokens.keys.length); // 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, lastKey, mapKeyType, typeDescriptor); + Object convertedMapKey = convertIfNecessary(null, null, lastKey, + mapKeyType.getResolvableType().resolve(), mapKeyType); Object oldValue = null; if (isExtractOldValueForEditor()) { oldValue = map.get(convertedMapKey); @@ -368,7 +367,7 @@ else if (propValue instanceof Map map) { // Pass full property name and old value in here, since we want full // conversion ability for map values. Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), - mapValueType, ph.nested(tokens.keys.length)); + mapValueType.getResolvableType().resolve(), mapValueType); map.put(convertedMapKey, convertedMapValue); } @@ -462,7 +461,9 @@ private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) ph.setValue(valueToApply); } catch (TypeMismatchException ex) { - throw ex; + if (!ph.setValueFallbackIfPossible(pv.getValue())) { + throw ex; + } } catch (InvocationTargetException ex) { PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent( @@ -657,22 +658,32 @@ else if (value instanceof List list) { growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); value = list.get(index); } - else if (value instanceof Set set) { - // Apply index to Iterator in case of a Set. + else if (value instanceof Iterable iterable) { + // Apply index to Iterator in case of a Set/Collection/Iterable. int index = Integer.parseInt(key); - if (index < 0 || index >= set.size()) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Cannot get element with index " + index + " from Set of size " + - set.size() + ", accessed using property path '" + propertyName + "'"); + if (value instanceof Collection coll) { + if (index < 0 || index >= coll.size()) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Cannot get element with index " + index + " from Collection of size " + + coll.size() + ", accessed using property path '" + propertyName + "'"); + } } - Iterator it = set.iterator(); - for (int j = 0; it.hasNext(); j++) { + Iterator it = iterable.iterator(); + boolean found = false; + int currIndex = 0; + for (; it.hasNext(); currIndex++) { Object elem = it.next(); - if (j == index) { + if (currIndex == index) { value = elem; + found = true; break; } } + if (!found) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Cannot get element with index " + index + " from Iterable of size " + + currIndex + ", accessed using property path '" + propertyName + "'"); + } } else if (value instanceof Map map) { Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); @@ -685,13 +696,17 @@ else if (value instanceof Map map) { else { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Property referenced in indexed property path '" + propertyName + - "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); + "' is neither an array nor a List/Set/Collection/Iterable nor a Map; " + + "returned value was [" + value + "]"); } indexedPropertyName.append(PROPERTY_KEY_PREFIX).append(key).append(PROPERTY_KEY_SUFFIX); } } return value; } + catch (InvalidPropertyException ex) { + throw ex; + } catch (IndexOutOfBoundsException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Index of out of bounds in property path '" + propertyName + "'", ex); @@ -756,7 +771,7 @@ private Object growArrayIfNecessary(Object array, int index, String name) { } int length = Array.getLength(array); if (index >= length && index < this.autoGrowCollectionLimit) { - Class componentType = array.getClass().getComponentType(); + Class componentType = array.getClass().componentType(); Object newArray = Array.newInstance(componentType, index + 1); System.arraycopy(array, 0, newArray, 0, length); for (int i = length; i < Array.getLength(newArray); i++) { @@ -830,8 +845,10 @@ protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(St * @return the PropertyAccessor instance, either cached or newly created */ private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) { - if (this.nestedPropertyAccessors == null) { - this.nestedPropertyAccessors = new HashMap<>(); + Map nestedAccessors = this.nestedPropertyAccessors; + if (nestedAccessors == null) { + nestedAccessors = new HashMap<>(); + this.nestedPropertyAccessors = nestedAccessors; } // Get value of bean property. PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); @@ -847,7 +864,7 @@ private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nested } // Lookup cached sub-PropertyAccessor, create new one if not found. - AbstractNestablePropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName); + AbstractNestablePropertyAccessor nestedPa = nestedAccessors.get(canonicalName); if (nestedPa == null || nestedPa.getWrappedInstance() != ObjectUtils.unwrapOptional(value)) { if (logger.isTraceEnabled()) { logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'"); @@ -856,7 +873,7 @@ private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nested // Inherit all type-specific PropertyEditors. copyDefaultEditorsTo(nestedPa); copyCustomEditorsTo(nestedPa, canonicalName); - this.nestedPropertyAccessors.put(canonicalName, nestedPa); + nestedAccessors.put(canonicalName, nestedPa); } else { if (logger.isTraceEnabled()) { @@ -887,11 +904,11 @@ private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { private Object newValue(Class type, @Nullable TypeDescriptor desc, String name) { try { if (type.isArray()) { - Class componentType = type.getComponentType(); + 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.getComponentType(), 0)); + Array.set(array, 0, Array.newInstance(componentType.componentType(), 0)); return array; } else { @@ -963,11 +980,11 @@ private int getPropertyNameKeyEnd(String propertyName, int startIndex) { int length = propertyName.length(); for (int i = startIndex; i < length; i++) { switch (propertyName.charAt(i)) { - case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: + case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> { // The property name contains opening prefix(es)... unclosedPrefixes++; - break; - case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: + } + case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> { if (unclosedPrefixes == 0) { // No unclosed prefix(es) in the property name (left) -> // this is the suffix we are looking for. @@ -978,13 +995,12 @@ private int getPropertyNameKeyEnd(String propertyName, int startIndex) { // just one that occurred within the property name. unclosedPrefixes--; } - break; + } } } return -1; } - @Override public String toString() { String className = getClass().getName(); @@ -1030,19 +1046,16 @@ public boolean isWritable() { public abstract ResolvableType getResolvableType(); - @Nullable - public Class getMapKeyType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0); + public TypeDescriptor getMapKeyType(int nestingLevel) { + return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0)); } - @Nullable - public Class getMapValueType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1); + public TypeDescriptor getMapValueType(int nestingLevel) { + return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1)); } - @Nullable - public Class getCollectionType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); + public TypeDescriptor getCollectionType(int nestingLevel) { + return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric()); } @Nullable @@ -1052,6 +1065,10 @@ public Class getCollectionType(int nestingLevel) { public abstract Object getValue() throws Exception; public abstract void setValue(@Nullable Object value) throws Exception; + + public boolean setValueFallbackIfPossible(@Nullable Object value) { + return false; + } } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java b/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java index 10bcf80400bf..a07cae6d3b84 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java @@ -69,7 +69,7 @@ public BeanInstantiationException(Class beanClass, String msg, @Nullable Thro * @param cause the root cause * @since 4.3 */ - public BeanInstantiationException(Constructor constructor, String msg, @Nullable Throwable cause) { + public BeanInstantiationException(Constructor constructor, @Nullable String msg, @Nullable Throwable cause) { super("Failed to instantiate [" + constructor.getDeclaringClass().getName() + "]: " + msg, cause); this.beanClass = constructor.getDeclaringClass(); this.constructor = constructor; @@ -84,7 +84,7 @@ public BeanInstantiationException(Constructor constructor, String msg, @Nulla * @param cause the root cause * @since 4.3 */ - public BeanInstantiationException(Method constructingMethod, String msg, @Nullable Throwable cause) { + public BeanInstantiationException(Method constructingMethod, @Nullable String msg, @Nullable Throwable cause) { super("Failed to instantiate [" + constructingMethod.getReturnType().getName() + "]: " + msg, cause); this.beanClass = constructingMethod.getReturnType(); this.constructor = null; 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 28d09d49ff06..f5c8a854ad48 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java @@ -90,7 +90,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return this.name.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.value); + return ObjectUtils.nullSafeHash(this.name, this.value); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 3df583a48a6b..cedf0408f2a3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,15 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; -import java.net.URI; -import java.net.URL; -import java.time.temporal.Temporal; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.full.KClasses; @@ -609,6 +605,22 @@ public static Class findPropertyType(String propertyName, @Nullable Class. return Object.class; } + /** + * Determine whether the specified property has a unique write method, + * i.e. is writable but does not declare overloaded setter methods. + * @param pd the PropertyDescriptor for the property + * @return {@code true} if writable and unique, {@code false} otherwise + * @since 6.1.4 + */ + public static boolean hasUniqueWriteMethod(PropertyDescriptor pd) { + if (pd instanceof GenericTypeAwarePropertyDescriptor gpd) { + return gpd.hasUniqueWriteMethod(); + } + else { + return (pd.getWriteMethod() != null); + } + } + /** * Obtain a new MethodParameter object for the write method of the * specified property. @@ -660,30 +672,26 @@ public static String[] getParameterNames(Constructor ctor) { */ public static boolean isSimpleProperty(Class type) { Assert.notNull(type, "'type' must not be null"); - return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType())); + return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.componentType())); } /** - * Check if the given type represents a "simple" value type: a primitive or - * primitive wrapper, an enum, a String or other CharSequence, a Number, a - * Date, a Temporal, a URI, a URL, a Locale, or a Class. + * Check if the given type represents a "simple" value type for + * bean property and data binding purposes: + * a primitive or primitive wrapper, an {@code Enum}, a {@code String} + * or other {@code CharSequence}, a {@code Number}, a {@code Date}, + * a {@code Temporal}, a {@code UUID}, a {@code URI}, a {@code URL}, + * a {@code Locale}, or a {@code Class}. *

{@code Void} and {@code void} are not considered simple value types. + *

As of 6.1, this method delegates to {@link ClassUtils#isSimpleValueType} + * as-is but could potentially add further rules for bean property purposes. * @param type the type to check * @return whether the given type represents a "simple" value type * @see #isSimpleProperty(Class) + * @see ClassUtils#isSimpleValueType(Class) */ public static boolean isSimpleValueType(Class type) { - return (Void.class != type && void.class != type && - (ClassUtils.isPrimitiveOrWrapper(type) || - Enum.class.isAssignableFrom(type) || - CharSequence.class.isAssignableFrom(type) || - Number.class.isAssignableFrom(type) || - Date.class.isAssignableFrom(type) || - Temporal.class.isAssignableFrom(type) || - URI.class == type || - URL.class == type || - Locale.class == type || - Class.class == type)); + return ClassUtils.isSimpleValueType(type); } @@ -805,7 +813,7 @@ private static void copyProperties(Object source, Object target, @Nullable Class if (sourcePd != null) { Method readMethod = sourcePd.getReadMethod(); if (readMethod != null) { - if (isAssignable(writeMethod, readMethod)) { + if (isAssignable(writeMethod, readMethod, sourcePd, targetPd)) { try { ReflectionUtils.makeAccessible(readMethod); Object value = readMethod.invoke(source); @@ -823,7 +831,9 @@ private static void copyProperties(Object source, Object target, @Nullable Class } } - private static boolean isAssignable(Method writeMethod, Method readMethod) { + private static boolean isAssignable(Method writeMethod, Method readMethod, + PropertyDescriptor sourcePd, PropertyDescriptor targetPd) { + Type paramType = writeMethod.getGenericParameterTypes()[0]; if (paramType instanceof Class clazz) { return ClassUtils.isAssignable(clazz, readMethod.getReturnType()); @@ -832,8 +842,8 @@ else if (paramType.equals(readMethod.getGenericReturnType())) { return true; } else { - ResolvableType sourceType = ResolvableType.forMethodReturnType(readMethod); - ResolvableType targetType = ResolvableType.forMethodParameter(writeMethod, 0); + ResolvableType sourceType = ((GenericTypeAwarePropertyDescriptor) sourcePd).getReadMethodType(); + ResolvableType targetType = ((GenericTypeAwarePropertyDescriptor) targetPd).getWriteMethodType(); // Ignore generic types in assignable check if either ResolvableType has unresolvable generics. return (sourceType.hasUnresolvableGenerics() || targetType.hasUnresolvableGenerics() ? ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) : @@ -853,13 +863,21 @@ private static class KotlinDelegate { * @see * https://kotlinlang.org/docs/reference/classes.html#constructors */ + @SuppressWarnings("unchecked") @Nullable public static Constructor findPrimaryConstructor(Class clazz) { try { - KFunction primaryCtor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); + KClass kClass = JvmClassMappingKt.getKotlinClass(clazz); + KFunction primaryCtor = KClasses.getPrimaryConstructor(kClass); if (primaryCtor == null) { return null; } + if (KotlinDetector.isInlineClass(clazz)) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + Assert.state(constructors.length == 1, + "Kotlin value classes annotated with @JvmInline are expected to have a single JVM constructor"); + return (Constructor) constructors[0]; + } Constructor constructor = ReflectJvmMapping.getJavaConstructor(primaryCtor); if (constructor == null) { throw new IllegalStateException( 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 4f610998ae03..93a9724d4420 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,13 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; +import org.apache.commons.logging.LogFactory; + +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.ReflectionUtils; /** @@ -183,23 +186,15 @@ public Object convertForProperty(@Nullable Object value, String propertyName) th throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName, "No property '" + propertyName + "' found"); } - TypeDescriptor td = cachedIntrospectionResults.getTypeDescriptor(pd); - if (td == null) { - td = cachedIntrospectionResults.addTypeDescriptor(pd, new TypeDescriptor(property(pd))); - } + TypeDescriptor td = ((GenericTypeAwarePropertyDescriptor) pd).getTypeDescriptor(); return convertForProperty(propertyName, null, value, td); } - private Property property(PropertyDescriptor pd) { - GenericTypeAwarePropertyDescriptor gpd = (GenericTypeAwarePropertyDescriptor) pd; - return new Property(gpd.getBeanClass(), gpd.getReadMethod(), gpd.getWriteMethod(), gpd.getName()); - } - @Override @Nullable protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); - return (pd != null ? new BeanPropertyHandler(pd) : null); + return (pd != null ? new BeanPropertyHandler((GenericTypeAwarePropertyDescriptor) pd) : null); } @Override @@ -234,44 +229,83 @@ public PropertyDescriptor getPropertyDescriptor(String propertyName) throws Inva private class BeanPropertyHandler extends PropertyHandler { - private final PropertyDescriptor pd; + private final GenericTypeAwarePropertyDescriptor pd; - public BeanPropertyHandler(PropertyDescriptor pd) { + public BeanPropertyHandler(GenericTypeAwarePropertyDescriptor pd) { super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null); this.pd = pd; } + @Override + public TypeDescriptor toTypeDescriptor() { + return this.pd.getTypeDescriptor(); + } + @Override public ResolvableType getResolvableType() { - return ResolvableType.forMethodReturnType(this.pd.getReadMethod()); + return this.pd.getReadMethodType(); } @Override - public TypeDescriptor toTypeDescriptor() { - return new TypeDescriptor(property(this.pd)); + public TypeDescriptor getMapValueType(int nestingLevel) { + return new TypeDescriptor( + this.pd.getReadMethodType().getNested(nestingLevel).asMap().getGeneric(1), + null, this.pd.getTypeDescriptor().getAnnotations()); + } + + @Override + public TypeDescriptor getCollectionType(int nestingLevel) { + return new TypeDescriptor( + this.pd.getReadMethodType().getNested(nestingLevel).asCollection().getGeneric(), + null, this.pd.getTypeDescriptor().getAnnotations()); } @Override @Nullable public TypeDescriptor nested(int level) { - return TypeDescriptor.nested(property(this.pd), level); + return this.pd.getTypeDescriptor().nested(level); } @Override @Nullable public Object getValue() throws Exception { Method readMethod = this.pd.getReadMethod(); + Assert.state(readMethod != null, "No read method available"); ReflectionUtils.makeAccessible(readMethod); return readMethod.invoke(getWrappedInstance(), (Object[]) null); } @Override public void setValue(@Nullable Object value) throws Exception { - Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor typeAwarePd ? - typeAwarePd.getWriteMethodForActualAccess() : this.pd.getWriteMethod()); + Method writeMethod = this.pd.getWriteMethodForActualAccess(); ReflectionUtils.makeAccessible(writeMethod); writeMethod.invoke(getWrappedInstance(), value); } + + @Override + public boolean setValueFallbackIfPossible(@Nullable Object value) { + try { + Method writeMethod = this.pd.getWriteMethodFallback(value != null ? value.getClass() : null); + if (writeMethod == null) { + writeMethod = this.pd.getUniqueWriteMethodFallback(); + if (writeMethod != null) { + // Conversion necessary as we would otherwise have received the method + // from the type-matching getWriteMethodFallback call above already + value = convertForProperty(this.pd.getName(), null, value, + new TypeDescriptor(new MethodParameter(writeMethod, 0))); + } + } + if (writeMethod != null) { + ReflectionUtils.makeAccessible(writeMethod); + writeMethod.invoke(getWrappedInstance(), value); + return true; + } + } + catch (Exception ex) { + LogFactory.getLog(BeanPropertyHandler.class).debug("Write method fallback failed", ex); + } + return false; + } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index e7b8d426e9f1..96d9f94548df 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -235,9 +234,6 @@ private static BeanInfo getBeanInfo(Class beanClass) throws IntrospectionExce /** PropertyDescriptor objects keyed by property name String. */ private final Map propertyDescriptors; - /** TypeDescriptor objects keyed by PropertyDescriptor. */ - private final ConcurrentMap typeDescriptorCache; - /** * Create a new CachedIntrospectionResults instance for the given class. @@ -300,8 +296,6 @@ private CachedIntrospectionResults(Class beanClass) throws BeansException { // - accessor method directly referring to instance field of same name // - same convention for component accessors of Java 15 record classes introspectPlainAccessors(beanClass, readMethodNames); - - this.typeDescriptorCache = new ConcurrentReferenceHashMap<>(); } catch (IntrospectionException ex) { throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex); @@ -410,14 +404,4 @@ private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class bean } } - TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) { - TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td); - return (existing != null ? existing : td); - } - - @Nullable - TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) { - return this.typeDescriptorCache.get(pd); - } - } 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 0a9b39e2e477..145a5cff9919 100644 --- a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -101,19 +101,34 @@ private class FieldPropertyHandler extends PropertyHandler { private final Field field; + private final ResolvableType resolvableType; + public FieldPropertyHandler(Field field) { super(field.getType(), true, true); this.field = field; + this.resolvableType = ResolvableType.forField(this.field); } @Override public TypeDescriptor toTypeDescriptor() { - return new TypeDescriptor(this.field); + return new TypeDescriptor(this.resolvableType, this.field.getType(), this.field.getAnnotations()); } @Override public ResolvableType getResolvableType() { - return ResolvableType.forField(this.field); + return this.resolvableType; + } + + @Override + public TypeDescriptor getMapValueType(int nestingLevel) { + return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asMap().getGeneric(1), + null, this.field.getAnnotations()); + } + + @Override + public TypeDescriptor getCollectionType(int nestingLevel) { + return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asCollection().getGeneric(), + null, this.field.getAnnotations()); } @Override 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 43632a665193..804ef9d21b31 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; @@ -192,14 +193,14 @@ private PropertyDescriptor findExistingPropertyDescriptor(String propertyName, C if (pd instanceof IndexedPropertyDescriptor indexedPd) { candidateType = indexedPd.getIndexedPropertyType(); if (candidateName.equals(propertyName) && - (candidateType.equals(propertyType) || candidateType.equals(propertyType.getComponentType()))) { + (candidateType.equals(propertyType) || candidateType.equals(propertyType.componentType()))) { return pd; } } else { candidateType = pd.getPropertyType(); if (candidateName.equals(propertyName) && - (candidateType.equals(propertyType) || propertyType.equals(candidateType.getComponentType()))) { + (candidateType.equals(propertyType) || propertyType.equals(candidateType.componentType()))) { return pd; } } @@ -345,7 +346,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return (ObjectUtils.nullSafeHashCode(getReadMethod()) * 29 + ObjectUtils.nullSafeHashCode(getWriteMethod())); + return Objects.hash(getReadMethod(), getWriteMethod()); } @Override @@ -500,11 +501,8 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(getReadMethod()); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getWriteMethod()); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getIndexedReadMethod()); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getIndexedWriteMethod()); - return hashCode; + return Objects.hash(getReadMethod(), getWriteMethod(), + getIndexedReadMethod(), getIndexedWriteMethod()); } @Override @@ -526,20 +524,7 @@ static class PropertyDescriptorComparator implements Comparator ambiguousWriteMethods; + private Set ambiguousWriteMethods; + + private volatile boolean ambiguousWriteMethodsLogged; @Nullable private MethodParameter writeMethodParameter; + @Nullable + private volatile ResolvableType writeMethodType; + + @Nullable + private ResolvableType readMethodType; + + @Nullable + private volatile TypeDescriptor typeDescriptor; + @Nullable private Class propertyType; @@ -107,7 +120,8 @@ public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyNam } if (this.readMethod != null) { - this.propertyType = GenericTypeResolver.resolveReturnType(this.readMethod, this.beanClass); + this.readMethodType = ResolvableType.forMethodReturnType(this.readMethod, this.beanClass); + this.propertyType = this.readMethodType.resolve(this.readMethod.getReturnType()); } else if (this.writeMethodParameter != null) { this.propertyType = this.writeMethodParameter.getParameterType(); @@ -135,21 +149,69 @@ public Method getWriteMethod() { public Method getWriteMethodForActualAccess() { Assert.state(this.writeMethod != null, "No write method available"); - Set ambiguousCandidates = this.ambiguousWriteMethods; - if (ambiguousCandidates != null) { - this.ambiguousWriteMethods = null; + if (this.ambiguousWriteMethods != null && !this.ambiguousWriteMethodsLogged) { + this.ambiguousWriteMethodsLogged = true; LogFactory.getLog(GenericTypeAwarePropertyDescriptor.class).debug("Non-unique JavaBean property '" + getName() + "' being accessed! Ambiguous write methods found next to actually used [" + - this.writeMethod + "]: " + ambiguousCandidates); + this.writeMethod + "]: " + this.ambiguousWriteMethods); } return this.writeMethod; } + @Nullable + public Method getWriteMethodFallback(@Nullable Class valueType) { + if (this.ambiguousWriteMethods != null) { + for (Method method : this.ambiguousWriteMethods) { + Class paramType = method.getParameterTypes()[0]; + if (valueType != null ? paramType.isAssignableFrom(valueType) : !paramType.isPrimitive()) { + return method; + } + } + } + return null; + } + + @Nullable + public Method getUniqueWriteMethodFallback() { + if (this.ambiguousWriteMethods != null && this.ambiguousWriteMethods.size() == 1) { + return this.ambiguousWriteMethods.iterator().next(); + } + return null; + } + + public boolean hasUniqueWriteMethod() { + return (this.writeMethod != null && this.ambiguousWriteMethods == null); + } + public MethodParameter getWriteMethodParameter() { Assert.state(this.writeMethodParameter != null, "No write method available"); return this.writeMethodParameter; } + public ResolvableType getWriteMethodType() { + ResolvableType writeMethodType = this.writeMethodType; + if (writeMethodType == null) { + writeMethodType = ResolvableType.forMethodParameter(getWriteMethodParameter()); + this.writeMethodType = writeMethodType; + } + return writeMethodType; + } + + public ResolvableType getReadMethodType() { + Assert.state(this.readMethodType != null, "No read method available"); + return this.readMethodType; + } + + public TypeDescriptor getTypeDescriptor() { + TypeDescriptor typeDescriptor = this.typeDescriptor; + if (typeDescriptor == null) { + Property property = new Property(getBeanClass(), getReadMethod(), getWriteMethod(), getName()); + typeDescriptor = new TypeDescriptor(property); + this.typeDescriptor = typeDescriptor; + } + return typeDescriptor; + } + @Override @Nullable public Class getPropertyType() { @@ -172,10 +234,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = getBeanClass().hashCode(); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getReadMethod()); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getWriteMethod()); - return hashCode; + return Objects.hash(getBeanClass(), getReadMethod(), getWriteMethod()); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java index 8a0af42cded8..327643cbbf3f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java @@ -18,6 +18,8 @@ import java.beans.PropertyChangeEvent; +import org.springframework.lang.Nullable; + /** * Thrown when a bean property getter or setter method throws an exception, * analogous to an InvocationTargetException. @@ -38,7 +40,7 @@ public class MethodInvocationException extends PropertyAccessException { * @param propertyChangeEvent the PropertyChangeEvent that resulted in an exception * @param cause the Throwable raised by the invoked method */ - public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, Throwable cause) { + public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) { super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause); } diff --git a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java index 62543e9a8cf4..f52780e0ec9d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import java.util.Map; import java.util.Set; import java.util.Spliterator; -import java.util.Spliterators; import java.util.stream.Stream; import org.springframework.lang.Nullable; @@ -255,7 +254,7 @@ public Iterator iterator() { @Override public Spliterator spliterator() { - return Spliterators.spliterator(this.propertyValueList, 0); + return this.propertyValueList.spliterator(); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java index 564194e2f0aa..465b3ff8b1d8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,14 +91,14 @@ private static int getNestedPropertySeparatorIndex(String propertyPath, boolean int i = (last ? length - 1 : 0); while (last ? i >= 0 : i < length) { switch (propertyPath.charAt(i)) { - case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: - case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: + case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR, PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> { inKey = !inKey; - break; - case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR: + } + case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR -> { if (!inKey) { return i; } + } } if (last) { i--; 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 2faf87e0e3c0..90c2aef90275 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,7 @@ public static Collection determineBasicProperties( setter = true; nameIndex = 3; } - else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) { + else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) { setter = false; nameIndex = 3; } @@ -152,7 +152,7 @@ public static Class findPropertyType(@Nullable Method readMethod, @Nullable M throw new IntrospectionException("Bad read method arg count: " + readMethod); } propertyType = readMethod.getReturnType(); - if (propertyType == Void.TYPE) { + if (propertyType == void.class) { throw new IntrospectionException("Read method returns void: " + readMethod); } } @@ -197,11 +197,11 @@ public static Class findIndexedPropertyType(String name, @Nullable Class p if (params.length != 1) { throw new IntrospectionException("Bad indexed read method arg count: " + indexedReadMethod); } - if (params[0] != Integer.TYPE) { + if (params[0] != int.class) { throw new IntrospectionException("Non int index to indexed read method: " + indexedReadMethod); } indexedPropertyType = indexedReadMethod.getReturnType(); - if (indexedPropertyType == Void.TYPE) { + if (indexedPropertyType == void.class) { throw new IntrospectionException("Indexed read method returns void: " + indexedReadMethod); } } @@ -211,7 +211,7 @@ public static Class findIndexedPropertyType(String name, @Nullable Class p if (params.length != 2) { throw new IntrospectionException("Bad indexed write method arg count: " + indexedWriteMethod); } - if (params[0] != Integer.TYPE) { + if (params[0] != int.class) { throw new IntrospectionException("Non int index to indexed write method: " + indexedWriteMethod); } if (indexedPropertyType != null) { @@ -233,7 +233,7 @@ else if (params[1].isAssignableFrom(indexedPropertyType)) { } if (propertyType != null && (!propertyType.isArray() || - propertyType.getComponentType() != indexedPropertyType)) { + propertyType.componentType() != indexedPropertyType)) { throw new IntrospectionException("Type mismatch between indexed and non-indexed methods: " + indexedReadMethod + " - " + indexedWriteMethod); } 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 b9f374e93f1d..00f567b0f67b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java @@ -197,7 +197,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return this.name.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.value); + return ObjectUtils.nullSafeHash(this.name, this.value); } @Override 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 123bb1d4270a..2fc9486c50f6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -35,6 +35,7 @@ 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; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -48,6 +49,7 @@ * @author Juergen Hoeller * @author Rob Harrop * @author Dave Syer + * @author Yanming Zhou * @since 2.0 * @see BeanWrapperImpl * @see SimpleTypeConverter @@ -139,13 +141,17 @@ public T convertIfNecessary(@Nullable String propertyName, @Nullable Object // Value not of required type? if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { - if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) && - convertedValue instanceof String text) { + if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType)) { TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor(); if (elementTypeDesc != null) { Class elementType = elementTypeDesc.getType(); - if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) { - convertedValue = StringUtils.commaDelimitedListToStringArray(text); + if (convertedValue instanceof String text) { + if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) { + convertedValue = StringUtils.commaDelimitedListToStringArray(text); + } + if (editor == null && String.class != elementType) { + editor = findDefaultEditor(elementType.arrayType()); + } } } } @@ -167,10 +173,21 @@ public T convertIfNecessary(@Nullable String propertyName, @Nullable Object else if (requiredType.isArray()) { // Array required -> apply appropriate conversion of elements. if (convertedValue instanceof String text && - Enum.class.isAssignableFrom(requiredType.getComponentType())) { + Enum.class.isAssignableFrom(requiredType.componentType())) { convertedValue = StringUtils.commaDelimitedListToStringArray(text); } - return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType()); + return (T) convertToTypedArray(convertedValue, propertyName, requiredType.componentType()); + } + else if (convertedValue.getClass().isArray()) { + if (Collection.class.isAssignableFrom(requiredType)) { + convertedValue = convertToTypedCollection(CollectionUtils.arrayToList(convertedValue), + propertyName, requiredType, typeDescriptor); + standardConversion = true; + } + else if (Array.getLength(convertedValue) == 1) { + convertedValue = Array.get(convertedValue, 0); + standardConversion = true; + } } else if (convertedValue instanceof Collection coll) { // Convert elements to target type, if determined. @@ -182,10 +199,6 @@ else if (convertedValue instanceof Map map) { convertedValue = convertToTypedMap(map, propertyName, requiredType, typeDescriptor); standardConversion = true; } - if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) { - convertedValue = Array.get(convertedValue, 0); - standardConversion = true; - } if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) { // We can stringify any primitive value... return (T) convertedValue.toString(); @@ -441,7 +454,7 @@ private Object convertToTypedArray(Object input, @Nullable String propertyName, } else if (input.getClass().isArray()) { // Convert array elements, if necessary. - if (componentType.equals(input.getClass().getComponentType()) && + if (componentType.equals(input.getClass().componentType()) && !this.propertyEditorRegistry.hasCustomEditorForElement(componentType, propertyName)) { return input; } @@ -502,12 +515,11 @@ private Collection convertToTypedCollection(Collection original, @Nullable Collection convertedCopy; try { - if (approximable) { + if (approximable && requiredType.isInstance(original)) { convertedCopy = CollectionFactory.createApproximateCollection(original, original.size()); } else { - convertedCopy = (Collection) - ReflectionUtils.accessibleConstructor(requiredType).newInstance(); + convertedCopy = CollectionFactory.createCollection(requiredType, original.size()); } } catch (Throwable ex) { @@ -577,12 +589,11 @@ private Collection convertToTypedCollection(Collection original, @Nullable Map convertedCopy; try { - if (approximable) { + if (approximable && requiredType.isInstance(original)) { convertedCopy = CollectionFactory.createApproximateMap(original, original.size()); } else { - convertedCopy = (Map) - ReflectionUtils.accessibleConstructor(requiredType).newInstance(); + convertedCopy = CollectionFactory.createMap(requiredType, original.size()); } } catch (Throwable ex) { 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 c0ad9408e932..2351382512c9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java @@ -42,7 +42,7 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport @Override @Nullable public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { - return convertIfNecessary(value, requiredType, TypeDescriptor.valueOf(requiredType)); + return convertIfNecessary(null, value, requiredType, TypeDescriptor.valueOf(requiredType)); } @Override @@ -50,7 +50,7 @@ public T convertIfNecessary(@Nullable Object value, @Nullable Class requi public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { - return convertIfNecessary(value, requiredType, + return convertIfNecessary((methodParam != null ? methodParam.getParameterName() : null), value, requiredType, (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType))); } @@ -59,7 +59,7 @@ public T convertIfNecessary(@Nullable Object value, @Nullable Class requi public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) throws TypeMismatchException { - return convertIfNecessary(value, requiredType, + return convertIfNecessary((field != null ? field.getName() : null), value, requiredType, (field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType))); } @@ -68,9 +68,17 @@ public T convertIfNecessary(@Nullable Object value, @Nullable Class requi public 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, + @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { + Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); try { - return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor); + return this.typeConverterDelegate.convertIfNecessary( + propertyName, null, value, requiredType, typeDescriptor); } catch (ConverterNotFoundException | IllegalStateException ex) { throw new ConversionNotSupportedException(value, requiredType, ex); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java index cac597c28ec7..9290a7153722 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 @@ -94,7 +94,7 @@ public BeanCreationException(String beanName, String msg, Throwable cause) { * @param beanName the name of the bean requested * @param msg the detail message */ - public BeanCreationException(@Nullable String resourceDescription, @Nullable String beanName, String msg) { + public BeanCreationException(@Nullable String resourceDescription, @Nullable String beanName, @Nullable String msg) { super("Error creating bean with name '" + beanName + "'" + (resourceDescription != null ? " defined in " + resourceDescription : "") + ": " + msg); this.resourceDescription = resourceDescription; @@ -110,7 +110,7 @@ public BeanCreationException(@Nullable String resourceDescription, @Nullable Str * @param msg the detail message * @param cause the root cause */ - public BeanCreationException(@Nullable String resourceDescription, String beanName, String msg, Throwable cause) { + public BeanCreationException(@Nullable String resourceDescription, String beanName, @Nullable String msg, Throwable cause) { this(resourceDescription, beanName, msg); initCause(cause); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index 696a9c1cd59e..8973cf271e5e 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 @@ -170,6 +170,8 @@ public interface BeanFactory { * Return an instance, which may be shared or independent, of the specified bean. *

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

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

This method goes into {@link ListableBeanFactory} by-type lookup territory * but may also be translated into a conventional by-name lookup based on the name * of the given type. For more extensive retrieval operations across sets of beans, @@ -239,7 +243,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. 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 eed896e1b4f0..0a4731f904c5 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Member; +import java.util.Objects; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; @@ -190,7 +191,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return (this.field != null ? this.field.hashCode() : ObjectUtils.nullSafeHashCode(this.methodParameter)); + return Objects.hash(this.field, this.methodParameter); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java b/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java index 29c4b47349f8..d11a5a16f823 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 @@ -44,7 +44,7 @@ public class UnsatisfiedDependencyException extends BeanCreationException { * @param msg the detail message */ public UnsatisfiedDependencyException( - @Nullable String resourceDescription, @Nullable String beanName, String propertyName, String msg) { + @Nullable String resourceDescription, @Nullable String beanName, String propertyName, @Nullable String msg) { super(resourceDescription, beanName, "Unsatisfied dependency expressed through bean property '" + propertyName + "'" + @@ -75,7 +75,7 @@ public UnsatisfiedDependencyException( * @since 4.3 */ public UnsatisfiedDependencyException( - @Nullable String resourceDescription, @Nullable String beanName, @Nullable InjectionPoint injectionPoint, String msg) { + @Nullable String resourceDescription, @Nullable String beanName, @Nullable InjectionPoint injectionPoint, @Nullable String msg) { super(resourceDescription, beanName, "Unsatisfied dependency expressed through " + injectionPoint + 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 fad36ac5bf08..0fdc535ec4b2 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ *

Autowired Constructors

*

Only one constructor of any given bean class may declare this annotation with the * {@link #required} attribute set to {@code true}, indicating the constructor - * to autowire when used as a Spring bean. Furthermore, if the {@code required} + * to be autowired when used as a Spring bean. Furthermore, if the {@code required} * attribute is set to {@code true}, only a single constructor may be annotated * with {@code @Autowired}. If multiple non-required constructors declare the * annotation, they will be considered as candidates for autowiring. The constructor 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 17e556315972..cbfe62cdafbb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.CodeWarnings; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; @@ -159,6 +160,9 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor, PriorityOrdered, BeanFactoryAware { + private static final Constructor[] EMPTY_CONSTRUCTOR_ARRAY = new Constructor[0]; + + protected final Log logger = LogFactory.getLog(getClass()); private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(4); @@ -193,9 +197,10 @@ public AutowiredAnnotationBeanPostProcessor() { this.autowiredAnnotationTypes.add(Autowired.class); this.autowiredAnnotationTypes.add(Value.class); + ClassLoader classLoader = AutowiredAnnotationBeanPostProcessor.class.getClassLoader(); try { this.autowiredAnnotationTypes.add((Class) - ClassUtils.forName("jakarta.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); + ClassUtils.forName("jakarta.inject.Inject", classLoader)); logger.trace("'jakarta.inject.Inject' annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { @@ -204,7 +209,7 @@ public AutowiredAnnotationBeanPostProcessor() { try { this.autowiredAnnotationTypes.add((Class) - ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); + ClassUtils.forName("javax.inject.Inject", classLoader)); logger.trace("'javax.inject.Inject' annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { @@ -285,7 +290,25 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + // Register externally managed config members on bean definition. findInjectionMetadata(beanName, beanType, beanDefinition); + + // Use opportunity to clear caches which are not needed after singleton instantiation. + // The injectionMetadataCache itself is left intact since it cannot be reliably + // reconstructed in terms of externally managed config members otherwise. + if (beanDefinition.isSingleton()) { + this.candidateConstructorsCache.remove(beanType); + // With actual lookup overrides, keep it intact along with bean definition. + if (!beanDefinition.hasMethodOverrides()) { + this.lookupMethodsChecked.remove(beanName); + } + } + } + + @Override + public void resetBeanDefinition(String beanName) { + this.lookupMethodsChecked.remove(beanName); + this.injectionMetadataCache.remove(beanName); } @Override @@ -323,12 +346,6 @@ private InjectionMetadata findInjectionMetadata(String beanName, Class beanTy return metadata; } - @Override - public void resetBeanDefinition(String beanName) { - this.lookupMethodsChecked.remove(beanName); - this.injectionMetadataCache.remove(beanName); - } - @Override public Class determineBeanType(Class beanClass, String beanName) throws BeanCreationException { checkLookupMethods(beanClass, beanName); @@ -428,7 +445,7 @@ else if (candidates.size() == 1 && logger.isInfoEnabled()) { "default constructor to fall back to: " + candidates.get(0)); } } - candidateConstructors = candidates.toArray(new Constructor[0]); + candidateConstructors = candidates.toArray(EMPTY_CONSTRUCTOR_ARRAY); } else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { candidateConstructors = new Constructor[] {rawCandidates[0]}; @@ -441,7 +458,7 @@ else if (nonSyntheticConstructors == 1 && primaryConstructor != null) { candidateConstructors = new Constructor[] {primaryConstructor}; } else { - candidateConstructors = new Constructor[0]; + candidateConstructors = EMPTY_CONSTRUCTOR_ARRAY; } this.candidateConstructorsCache.put(beanClass, candidateConstructors); } @@ -501,9 +518,9 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str } /** - * 'Native' processing method for direct calls with an arbitrary target instance, - * resolving all of its fields and methods which are annotated with one of the - * configured 'autowired' annotation types. + * Native processing method for direct calls with an arbitrary target + * instance, resolving all of its fields and methods which are annotated with + * one of the configured 'autowired' annotation types. * @param bean the target instance to process * @throws BeanCreationException if autowiring failed * @see #setAutowiredAnnotationTypes(Set) @@ -869,7 +886,7 @@ private Object[] resolveMethodArguments(Method method, Object bean, @Nullable St descriptors[i] = currDesc; try { Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeanNames, typeConverter); - if (arg == null && !this.required) { + if (arg == null && !this.required && !methodParam.isOptional()) { arguments = null; break; } @@ -968,8 +985,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()); @@ -978,57 +998,63 @@ 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)", AutowiredFieldValueResolver.class, - (!required) ? "forField" : "forRequiredField", field.getName()); + (!required ? "forField" : "forRequiredField"), field.getName()); AccessControl accessControl = AccessControl.forMember(field); if (!accessControl.isAccessibleFrom(targetClassName)) { return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); } - return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, - field.getName(), resolver, REGISTERED_BEAN_PARAMETER); + 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"); + (!required ? "forMethod" : "forRequiredMethod")); code.add("($S", method.getName()); if (method.getParameterCount() > 0) { + codeWarnings.detectDeprecation(method.getParameterTypes()); code.add(", $L", generateParameterTypesCode(method.getParameterTypes())); } code.add(")"); @@ -1038,6 +1064,7 @@ private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, code.add(".resolveAndInvoke($L, $L)", REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); } else { + codeWarnings.detectDeprecation(method); hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT); CodeBlock arguments = new AutowiredArgumentsCodeGenerator(this.target, method).generateCode(method.getParameterTypes()); @@ -1049,12 +1076,9 @@ private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, } private CodeBlock generateParameterTypesCode(Class[] parameterTypes) { - CodeBlock.Builder code = CodeBlock.builder(); - for (int i = 0; i < parameterTypes.length; i++) { - code.add(i != 0 ? ", " : ""); - code.add("$T.class", parameterTypes[i]); - } - return code.build(); + return CodeBlock.join(Arrays.stream(parameterTypes) + .map(parameterType -> CodeBlock.of("$T.class", parameterType)) + .toList(), ", "); } private void registerHints(RuntimeHints runtimeHints) { @@ -1085,7 +1109,6 @@ private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescr } } } - } } 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 a5a29e7c9b33..439b1fb30e47 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,23 +20,28 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** - * {@link RuntimeHintsRegistrar} for Jakarta annotations. - *

Hints are only registered if Jakarta inject is on the classpath. + * {@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) { - if (ClassUtils.isPresent("jakarta.inject.Inject", classLoader)) { - Stream.of("jakarta.inject.Inject", "jakarta.inject.Qualifier").forEach(annotationType -> - hints.reflection().registerType(ClassUtils.resolveClassName(annotationType, classLoader))); - } + // javax.inject.Provider is omitted from the list, since we do not currently load + // it via reflection. + Stream.of( + "jakarta.inject.Inject", + "jakarta.inject.Provider", + "jakarta.inject.Qualifier", + "javax.inject.Inject", + "javax.inject.Qualifier" + ).forEach(typeName -> hints.reflection().registerType(TypeReference.of(typeName))); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index cc926c37e5ca..cf552f996e00 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,11 +47,13 @@ * against {@link Qualifier qualifier annotations} on the field or parameter to be autowired. * Also supports suggested expression values through a {@link Value value} annotation. * - *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation, if available. + *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation (as well as its + * pre-Jakarta {@code javax.inject.Qualifier} equivalent), if available. * * @author Mark Fisher * @author Juergen Hoeller * @author Stephane Nicoll + * @author Sam Brannen * @since 2.5 * @see AutowireCandidateQualifier * @see Qualifier @@ -65,9 +67,10 @@ 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} annotation. + *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation (as well as + * its pre-Jakarta {@code javax.inject.Qualifier} equivalent), if available. */ @SuppressWarnings("unchecked") public QualifierAnnotationAutowireCandidateResolver() { @@ -76,14 +79,21 @@ public QualifierAnnotationAutowireCandidateResolver() { this.qualifierTypes.add((Class) ClassUtils.forName("jakarta.inject.Qualifier", QualifierAnnotationAutowireCandidateResolver.class.getClassLoader())); } + catch (ClassNotFoundException ex) { + // JSR-330 API (as included in Jakarta EE) not available - simply skip. + } + try { + this.qualifierTypes.add((Class) ClassUtils.forName("javax.inject.Qualifier", + QualifierAnnotationAutowireCandidateResolver.class.getClassLoader())); + } catch (ClassNotFoundException ex) { // JSR-330 API not available - simply skip. } } /** - * 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 +102,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) { @@ -282,7 +292,7 @@ protected boolean checkQualifier( } if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) && expectedValue instanceof String name && bdHolder.matchesName(name)) { - // Fall back on bean name (or alias) match + // Finally, check bean name (or alias) match continue; } if (actualValue == null && qualifier != null) { @@ -292,7 +302,7 @@ protected boolean checkQualifier( if (actualValue != null) { actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass()); } - if (!expectedValue.equals(actualValue)) { + if (!ObjectUtils.nullSafeEquals(expectedValue, actualValue)) { return false; } } @@ -333,8 +343,8 @@ public boolean isRequired(DependencyDescriptor descriptor) { */ @Override public boolean hasQualifier(DependencyDescriptor descriptor) { - for (Annotation ann : descriptor.getAnnotations()) { - if (isQualifier(ann.annotationType())) { + for (Annotation annotation : descriptor.getAnnotations()) { + if (isQualifier(annotation.annotationType())) { return true; } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java index 12cfc76b0833..1c5f68fe902f 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 @@ -191,16 +191,14 @@ private Object resolveValue(RegisteredBean registeredBean, Field field) { return value; } catch (BeansException ex) { - throw new UnsatisfiedDependencyException(null, beanName, - new InjectionPoint(field), ex); + throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); } } private Field getField(RegisteredBean registeredBean) { - Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), - this.fieldName); - Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " - + registeredBean.getBeanClass().getName()); + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), this.fieldName); + Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " + + registeredBean.getBeanClass().getName()); return field; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java index 68a06fd7dbfd..074e87df871b 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 @@ -16,10 +16,6 @@ package org.springframework.beans.factory.aot; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.util.List; import javax.lang.model.element.Modifier; @@ -29,14 +25,8 @@ import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; -import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.DependencyDescriptor; -import org.springframework.beans.factory.support.AutowireCandidateResolver; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.core.MethodParameter; import org.springframework.javapoet.ClassName; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -56,8 +46,6 @@ class BeanDefinitionMethodGenerator { private final RegisteredBean registeredBean; - private final Executable constructorOrFactoryMethod; - @Nullable private final String currentPropertyName; @@ -77,13 +65,8 @@ class BeanDefinitionMethodGenerator { RegisteredBean registeredBean, @Nullable String currentPropertyName, List aotContributions) { - RootBeanDefinition mbd = registeredBean.getMergedBeanDefinition(); - if (mbd.getInstanceSupplier() != null && aotContributions.isEmpty()) { - throw new IllegalArgumentException("Code generation is not supported for bean definitions declaring an instance supplier callback : " + mbd); - } this.methodGeneratorFactory = methodGeneratorFactory; this.registeredBean = registeredBean; - this.constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod(); this.currentPropertyName = currentPropertyName; this.aotContributions = aotContributions; } @@ -98,9 +81,8 @@ class BeanDefinitionMethodGenerator { MethodReference generateBeanDefinitionMethod(GenerationContext generationContext, BeanRegistrationsCode beanRegistrationsCode) { - registerRuntimeHintsIfNecessary(generationContext.getRuntimeHints()); BeanRegistrationCodeFragments codeFragments = getCodeFragments(generationContext, beanRegistrationsCode); - ClassName target = codeFragments.getTarget(this.registeredBean, this.constructorOrFactoryMethod); + ClassName target = codeFragments.getTarget(this.registeredBean); if (isWritablePackageName(target)) { GeneratedClass generatedClass = lookupGeneratedClass(generationContext, target); GeneratedMethods generatedMethods = generatedClass.getMethods().withPrefix(getName()); @@ -178,16 +160,18 @@ private GeneratedMethod generateBeanDefinitionMethod(GenerationContext generatio BeanRegistrationCodeFragments codeFragments, Modifier modifier) { BeanRegistrationCodeGenerator codeGenerator = new BeanRegistrationCodeGenerator( - className, generatedMethods, this.registeredBean, - this.constructorOrFactoryMethod, codeFragments); + className, generatedMethods, this.registeredBean, codeFragments); this.aotContributions.forEach(aotContribution -> aotContribution.applyTo(generationContext, codeGenerator)); + CodeWarnings codeWarnings = new CodeWarnings(); + codeWarnings.detectDeprecation(this.registeredBean.getBeanType()); return generatedMethods.add("getBeanDefinition", method -> { method.addJavadoc("Get the $L definition for '$L'.", (this.registeredBean.isInnerBean() ? "inner-bean" : "bean"), getName()); method.addModifiers(modifier, Modifier.STATIC); + codeWarnings.suppress(method); method.returns(BeanDefinition.class); method.addCode(codeGenerator.generateCode(generationContext)); }); @@ -218,52 +202,4 @@ private String getSimpleBeanName(String beanName) { return StringUtils.uncapitalize(beanName); } - private void registerRuntimeHintsIfNecessary(RuntimeHints runtimeHints) { - if (this.registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory dlbf) { - ProxyRuntimeHintsRegistrar registrar = new ProxyRuntimeHintsRegistrar(dlbf.getAutowireCandidateResolver()); - if (this.constructorOrFactoryMethod instanceof Method method) { - registrar.registerRuntimeHints(runtimeHints, method); - } - else if (this.constructorOrFactoryMethod instanceof Constructor constructor) { - registrar.registerRuntimeHints(runtimeHints, constructor); - } - } - } - - - private static class ProxyRuntimeHintsRegistrar { - - private final AutowireCandidateResolver candidateResolver; - - public ProxyRuntimeHintsRegistrar(AutowireCandidateResolver candidateResolver) { - this.candidateResolver = candidateResolver; - } - - public void registerRuntimeHints(RuntimeHints runtimeHints, Method method) { - Class[] parameterTypes = method.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - MethodParameter methodParam = new MethodParameter(method, 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)) { - runtimeHints.proxies().registerJdkProxy(proxyType.getInterfaces()); - } - } - } - } 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 c4efb48dcd00..f183d2b05e14 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -33,14 +34,20 @@ import java.util.function.Predicate; 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; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.BeanUtils; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; 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.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AutowireCandidateQualifier; @@ -85,17 +92,22 @@ class BeanDefinitionPropertiesCodeGenerator { private final Predicate attributeFilter; - private final BeanDefinitionPropertyValueCodeGenerator valueCodeGenerator; + private final ValueCodeGenerator valueCodeGenerator; BeanDefinitionPropertiesCodeGenerator(RuntimeHints hints, Predicate attributeFilter, GeneratedMethods generatedMethods, + List additionalDelegates, BiFunction customValueCodeGenerator) { this.hints = hints; this.attributeFilter = attributeFilter; - this.valueCodeGenerator = new BeanDefinitionPropertyValueCodeGenerator(generatedMethods, - (object, type) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), object)); + List allDelegates = new ArrayList<>(); + allDelegates.add((valueCodeGenerator, value) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); + allDelegates.addAll(additionalDelegates); + allDelegates.addAll(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES); + allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); + this.valueCodeGenerator = ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); } @@ -128,6 +140,8 @@ CodeBlock generateCode(RootBeanDefinition beanDefinition) { private void addInitDestroyMethods(Builder code, AbstractBeanDefinition beanDefinition, @Nullable String[] methodNames, String format) { + // For Publisher-based destroy methods + this.hints.reflection().registerType(TypeReference.of("org.reactivestreams.Publisher")); if (!ObjectUtils.isEmpty(methodNames)) { Class beanType = ClassUtils.getUserClass(beanDefinition.getResolvableType().toClass()); Arrays.stream(methodNames).forEach(methodName -> addInitDestroyHint(beanType, methodName)); @@ -160,44 +174,79 @@ 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); + } } } private void addConstructorArgumentValues(CodeBlock.Builder code, BeanDefinition beanDefinition) { - Map argumentValues = - beanDefinition.getConstructorArgumentValues().getIndexedArgumentValues(); - if (!argumentValues.isEmpty()) { - argumentValues.forEach((index, valueHolder) -> { - CodeBlock valueCode = generateValue(valueHolder.getName(), valueHolder.getValue()); + ConstructorArgumentValues constructorValues = beanDefinition.getConstructorArgumentValues(); + Map indexedValues = constructorValues.getIndexedArgumentValues(); + if (!indexedValues.isEmpty()) { + indexedValues.forEach((index, valueHolder) -> { + Object value = valueHolder.getValue(); + CodeBlock valueCode = castIfNecessary(value == null, Object.class, + generateValue(valueHolder.getName(), value)); code.addStatement( "$L.getConstructorArgumentValues().addIndexedArgumentValue($L, $L)", BEAN_DEFINITION_VARIABLE, index, valueCode); }); } + List genericValues = constructorValues.getGenericArgumentValues(); + if (!genericValues.isEmpty()) { + genericValues.forEach(valueHolder -> { + String valueName = valueHolder.getName(); + CodeBlock valueCode = generateValue(valueName, valueHolder.getValue()); + if (valueName != null) { + CodeBlock valueTypeCode = this.valueCodeGenerator.generateCode(valueHolder.getType()); + code.addStatement( + "$L.getConstructorArgumentValues().addGenericArgumentValue(new $T($L, $L, $S))", + BEAN_DEFINITION_VARIABLE, ValueHolder.class, valueCode, valueTypeCode, valueName); + } + 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)", + BEAN_DEFINITION_VARIABLE, valueCode); + } + }); + } } private void addPropertyValues(CodeBlock.Builder code, RootBeanDefinition beanDefinition) { MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); if (!propertyValues.isEmpty()) { + Class infrastructureType = getInfrastructureType(beanDefinition); + Map writeMethods = (infrastructureType != Object.class) ? getWriteMethods(infrastructureType) : Collections.emptyMap(); for (PropertyValue propertyValue : propertyValues) { String name = propertyValue.getName(); CodeBlock valueCode = generateValue(name, propertyValue.getValue()); code.addStatement("$L.getPropertyValues().addPropertyValue($S, $L)", - BEAN_DEFINITION_VARIABLE, propertyValue.getName(), valueCode); - } - Class infrastructureType = getInfrastructureType(beanDefinition); - if (infrastructureType != Object.class) { - Map writeMethods = getWriteMethods(infrastructureType); - for (PropertyValue propertyValue : propertyValues) { - Method writeMethod = writeMethods.get(propertyValue.getName()); - if (writeMethod != null) { - this.hints.reflection().registerMethod(writeMethod, ExecutableMode.INVOKE); - } + BEAN_DEFINITION_VARIABLE, name, valueCode); + Method writeMethod = writeMethods.get(name); + if (writeMethod != null) { + registerReflectionHints(beanDefinition, writeMethod); } } } } + private void registerReflectionHints(RootBeanDefinition beanDefinition, Method writeMethod) { + this.hints.reflection().registerMethod(writeMethod, ExecutableMode.INVOKE); + // ReflectionUtils#findField searches recursively in the type hierarchy + Class searchType = beanDefinition.getTargetType(); + while (searchType != null && searchType != writeMethod.getDeclaringClass()) { + this.hints.reflection().registerType(searchType, MemberCategory.DECLARED_FIELDS); + searchType = searchType.getSuperclass(); + } + this.hints.reflection().registerType(writeMethod.getDeclaringClass(), MemberCategory.DECLARED_FIELDS); + } + private void addQualifiers(CodeBlock.Builder code, RootBeanDefinition beanDefinition) { Set qualifiers = beanDefinition.getQualifiers(); if (!qualifiers.isEmpty()) { @@ -311,12 +360,27 @@ 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. + * @param castNecessary whether a cast is necessary + * @param castType the type to cast to + * @param valueCode the code for the value + * @return the existing value or a form of {@code (castType) valueCode} if a + * cast is necessary + */ + private CodeBlock castIfNecessary(boolean castNecessary, Class castType, CodeBlock valueCode) { + return (castNecessary ? CodeBlock.of("($T) $L", castType, valueCode) : valueCode); + } + + static class PropertyNamesStack { private static final ThreadLocal> threadLocal = ThreadLocal.withInitial(ArrayDeque::new); static void push(@Nullable String name) { - String valueToSet = (name != null) ? name : ""; + String valueToSet = (name != null ? name : ""); threadLocal.get().push(valueToSet); } @@ -329,7 +393,6 @@ static String peek() { String value = threadLocal.get().peek(); return ("".equals(value) ? null : value); } - } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java deleted file mode 100644 index 0f31b2798b78..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java +++ /dev/null @@ -1,564 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.beans.factory.aot; - -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.function.BiFunction; -import java.util.stream.Stream; - -import org.springframework.aot.generate.GeneratedMethod; -import org.springframework.aot.generate.GeneratedMethods; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanReference; -import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.support.ManagedList; -import org.springframework.beans.factory.support.ManagedMap; -import org.springframework.beans.factory.support.ManagedSet; -import org.springframework.core.ResolvableType; -import org.springframework.javapoet.AnnotationSpec; -import org.springframework.javapoet.CodeBlock; -import org.springframework.javapoet.CodeBlock.Builder; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; - -/** - * Internal code generator used to generate code for a single value contained in - * a {@link BeanDefinition} property. - * - * @author Stephane Nicoll - * @author Phillip Webb - * @author Sebastien Deleuze - * @since 6.0 - */ -class BeanDefinitionPropertyValueCodeGenerator { - - static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null"); - - private final GeneratedMethods generatedMethods; - - private final List delegates; - - - BeanDefinitionPropertyValueCodeGenerator(GeneratedMethods generatedMethods, - @Nullable BiFunction customValueGenerator) { - - this.generatedMethods = generatedMethods; - this.delegates = new ArrayList<>(); - if (customValueGenerator != null) { - this.delegates.add(customValueGenerator::apply); - } - this.delegates.addAll(List.of( - new PrimitiveDelegate(), - new StringDelegate(), - new CharsetDelegate(), - new EnumDelegate(), - new ClassDelegate(), - new ResolvableTypeDelegate(), - new ArrayDelegate(), - new ManagedListDelegate(), - new ManagedSetDelegate(), - new ManagedMapDelegate(), - new ListDelegate(), - new SetDelegate(), - new MapDelegate(), - new BeanReferenceDelegate() - )); - } - - - CodeBlock generateCode(@Nullable Object value) { - ResolvableType type = ResolvableType.forInstance(value); - try { - return generateCode(value, type); - } - catch (Exception ex) { - throw new IllegalArgumentException(buildErrorMessage(value, type), ex); - } - } - - private CodeBlock generateCodeForElement(@Nullable Object value, ResolvableType type) { - try { - return generateCode(value, type); - } - catch (Exception ex) { - throw new IllegalArgumentException(buildErrorMessage(value, type), ex); - } - } - - private static String buildErrorMessage(@Nullable Object value, ResolvableType type) { - StringBuilder message = new StringBuilder("Failed to generate code for '"); - message.append(value).append("'"); - if (type != ResolvableType.NONE) { - message.append(" with type ").append(type); - } - return message.toString(); - } - - private CodeBlock generateCode(@Nullable Object value, ResolvableType type) { - if (value == null) { - return NULL_VALUE_CODE_BLOCK; - } - for (Delegate delegate : this.delegates) { - CodeBlock code = delegate.generateCode(value, type); - if (code != null) { - return code; - } - } - throw new IllegalArgumentException("Code generation does not support " + type); - } - - - /** - * Internal delegate used to support generation for a specific type. - */ - @FunctionalInterface - private interface Delegate { - - @Nullable - CodeBlock generateCode(Object value, ResolvableType type); - } - - - /** - * {@link Delegate} for {@code primitive} types. - */ - private static class PrimitiveDelegate implements Delegate { - - private static final Map CHAR_ESCAPES = Map.of( - '\b', "\\b", - '\t', "\\t", - '\n', "\\n", - '\f', "\\f", - '\r', "\\r", - '\"', "\"", - '\'', "\\'", - '\\', "\\\\" - ); - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Boolean || value instanceof Integer) { - return CodeBlock.of("$L", value); - } - if (value instanceof Byte) { - return CodeBlock.of("(byte) $L", value); - } - if (value instanceof Short) { - return CodeBlock.of("(short) $L", value); - } - if (value instanceof Long) { - return CodeBlock.of("$LL", value); - } - if (value instanceof Float) { - return CodeBlock.of("$LF", value); - } - if (value instanceof Double) { - return CodeBlock.of("(double) $L", value); - } - if (value instanceof Character character) { - return CodeBlock.of("'$L'", escape(character)); - } - return null; - } - - private String escape(char ch) { - String escaped = CHAR_ESCAPES.get(ch); - if (escaped != null) { - return escaped; - } - return (!Character.isISOControl(ch)) ? Character.toString(ch) - : String.format("\\u%04x", (int) ch); - } - } - - - /** - * {@link Delegate} for {@link String} types. - */ - private static class StringDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof String) { - return CodeBlock.of("$S", value); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link Charset} types. - */ - private static class CharsetDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Charset charset) { - return CodeBlock.of("$T.forName($S)", Charset.class, charset.name()); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link Enum} types. - */ - private static class EnumDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Enum enumValue) { - return CodeBlock.of("$T.$L", enumValue.getDeclaringClass(), - enumValue.name()); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link Class} types. - */ - private static class ClassDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Class clazz) { - return CodeBlock.of("$T.class", ClassUtils.getUserClass(clazz)); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link ResolvableType} types. - */ - private static class ResolvableTypeDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof ResolvableType resolvableType) { - return ResolvableTypeCodeGenerator.generateCode(resolvableType); - } - return null; - } - } - - - /** - * {@link Delegate} for {@code array} types. - */ - private class ArrayDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(@Nullable Object value, ResolvableType type) { - if (type.isArray()) { - ResolvableType componentType = type.getComponentType(); - Stream elements = Arrays.stream(ObjectUtils.toObjectArray(value)).map(component -> - BeanDefinitionPropertyValueCodeGenerator.this.generateCode(component, componentType)); - CodeBlock.Builder code = CodeBlock.builder(); - code.add("new $T {", type.toClass()); - code.add(elements.collect(CodeBlock.joining(", "))); - code.add("}"); - return code.build(); - } - return null; - } - } - - - /** - * Abstract {@link Delegate} for {@code Collection} types. - */ - private abstract class CollectionDelegate> implements Delegate { - - private final Class collectionType; - - private final CodeBlock emptyResult; - - public CollectionDelegate(Class collectionType, CodeBlock emptyResult) { - this.collectionType = collectionType; - this.emptyResult = emptyResult; - } - - @SuppressWarnings("unchecked") - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (this.collectionType.isInstance(value)) { - T collection = (T) value; - if (collection.isEmpty()) { - return this.emptyResult; - } - ResolvableType elementType = type.as(this.collectionType).getGeneric(); - return generateCollectionCode(elementType, collection); - } - return null; - } - - protected CodeBlock generateCollectionCode(ResolvableType elementType, T collection) { - return generateCollectionOf(collection, this.collectionType, elementType); - } - - protected final CodeBlock generateCollectionOf(Collection collection, - Class collectionType, ResolvableType elementType) { - Builder code = CodeBlock.builder(); - code.add("$T.of(", collectionType); - Iterator iterator = collection.iterator(); - while (iterator.hasNext()) { - Object element = iterator.next(); - code.add("$L", BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(element, elementType)); - if (iterator.hasNext()) { - code.add(", "); - } - } - code.add(")"); - return code.build(); - } - } - - - /** - * {@link Delegate} for {@link ManagedList} types. - */ - private class ManagedListDelegate extends CollectionDelegate> { - - public ManagedListDelegate() { - super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class)); - } - } - - - /** - * {@link Delegate} for {@link ManagedSet} types. - */ - private class ManagedSetDelegate extends CollectionDelegate> { - - public ManagedSetDelegate() { - super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class)); - } - } - - - /** - * {@link Delegate} for {@link ManagedMap} types. - */ - private class ManagedMapDelegate implements Delegate { - - private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class); - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof ManagedMap managedMap) { - return generateManagedMapCode(type, managedMap); - } - return null; - } - - private CodeBlock generateManagedMapCode(ResolvableType type, ManagedMap managedMap) { - if (managedMap.isEmpty()) { - return EMPTY_RESULT; - } - ResolvableType keyType = type.as(Map.class).getGeneric(0); - ResolvableType valueType = type.as(Map.class).getGeneric(1); - CodeBlock.Builder code = CodeBlock.builder(); - code.add("$T.ofEntries(", ManagedMap.class); - Iterator> iterator = managedMap.entrySet().iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - code.add("$T.entry($L,$L)", Map.class, - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getKey(), keyType), - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getValue(), valueType)); - if (iterator.hasNext()) { - code.add(", "); - } - } - code.add(")"); - return code.build(); - } - } - - - /** - * {@link Delegate} for {@link List} types. - */ - private class ListDelegate extends CollectionDelegate> { - - ListDelegate() { - super(List.class, CodeBlock.of("$T.emptyList()", Collections.class)); - } - } - - - /** - * {@link Delegate} for {@link Set} types. - */ - private class SetDelegate extends CollectionDelegate> { - - SetDelegate() { - super(Set.class, CodeBlock.of("$T.emptySet()", Collections.class)); - } - - @Override - protected CodeBlock generateCollectionCode(ResolvableType elementType, Set set) { - if (set instanceof LinkedHashSet) { - return CodeBlock.of("new $T($L)", LinkedHashSet.class, - generateCollectionOf(set, List.class, elementType)); - } - try { - set = orderForCodeConsistency(set); - } - catch (ClassCastException ex) { - // If elements are not comparable, just keep the original set - } - return super.generateCollectionCode(elementType, set); - } - - private Set orderForCodeConsistency(Set set) { - return new TreeSet(set); - } - } - - - /** - * {@link Delegate} for {@link Map} types. - */ - private class MapDelegate implements Delegate { - - private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class); - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Map map) { - return generateMapCode(type, map); - } - return null; - } - - private CodeBlock generateMapCode(ResolvableType type, Map map) { - if (map.isEmpty()) { - return EMPTY_RESULT; - } - ResolvableType keyType = type.as(Map.class).getGeneric(0); - ResolvableType valueType = type.as(Map.class).getGeneric(1); - if (map instanceof LinkedHashMap) { - return generateLinkedHashMapCode(map, keyType, valueType); - } - map = orderForCodeConsistency(map); - boolean useOfEntries = map.size() > 10; - CodeBlock.Builder code = CodeBlock.builder(); - code.add("$T" + ((!useOfEntries) ? ".of(" : ".ofEntries("), Map.class); - Iterator> iterator = map.entrySet().iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - CodeBlock keyCode = BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getKey(), keyType); - CodeBlock valueCode = BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getValue(), valueType); - if (!useOfEntries) { - code.add("$L, $L", keyCode, valueCode); - } - else { - code.add("$T.entry($L,$L)", Map.class, keyCode, valueCode); - } - if (iterator.hasNext()) { - code.add(", "); - } - } - code.add(")"); - return code.build(); - } - - private Map orderForCodeConsistency(Map map) { - return new TreeMap<>(map); - } - - private CodeBlock generateLinkedHashMapCode(Map map, - ResolvableType keyType, ResolvableType valueType) { - - GeneratedMethods generatedMethods = BeanDefinitionPropertyValueCodeGenerator.this.generatedMethods; - GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> { - method.addAnnotation(AnnotationSpec - .builder(SuppressWarnings.class) - .addMember("value", "{\"rawtypes\", \"unchecked\"}") - .build()); - method.returns(Map.class); - method.addStatement("$T map = new $T($L)", Map.class, - LinkedHashMap.class, map.size()); - map.forEach((key, value) -> method.addStatement("map.put($L, $L)", - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(key, keyType), - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(value, valueType))); - method.addStatement("return map"); - }); - return CodeBlock.of("$L()", generatedMethod.getName()); - } - } - - - /** - * {@link Delegate} for {@link BeanReference} types. - */ - private static class BeanReferenceDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof RuntimeBeanReference runtimeBeanReference && - runtimeBeanReference.getBeanType() != null) { - return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class, - runtimeBeanReference.getBeanType()); - } - else if (value instanceof BeanReference beanReference) { - return CodeBlock.of("new $T($S)", RuntimeBeanReference.class, - beanReference.getBeanName()); - } - return null; - } - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java new file mode 100644 index 000000000000..c4188077839a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.aot; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; +import org.springframework.aot.generate.ValueCodeGeneratorDelegates; +import org.springframework.aot.generate.ValueCodeGeneratorDelegates.CollectionDelegate; +import org.springframework.aot.generate.ValueCodeGeneratorDelegates.MapDelegate; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.javapoet.AnnotationSpec; +import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.Nullable; + +/** + * Code generator {@link Delegate} for common bean definition property values. + * + * @author Stephane Nicoll + * @since 6.1.2 + */ +abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { + + /** + * Return the {@link Delegate} implementations for common bean definition + * property value types. These are: + *
    + *
  • {@link ManagedList},
  • + *
  • {@link ManagedSet},
  • + *
  • {@link ManagedMap},
  • + *
  • {@link LinkedHashMap},
  • + *
  • {@link BeanReference},
  • + *
  • {@link TypedStringValue}.
  • + *
+ * When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the + * delegates for common value types}, this should be added first as they have + * special handling for list, set, and map. + */ + public static final List INSTANCES = List.of( + new ManagedListDelegate(), + new ManagedSetDelegate(), + new ManagedMapDelegate(), + new LinkedHashMapDelegate(), + new BeanReferenceDelegate(), + new TypedStringValueDelegate() + ); + + + /** + * {@link Delegate} for {@link ManagedList} types. + */ + private static class ManagedListDelegate extends CollectionDelegate> { + + public ManagedListDelegate() { + super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class)); + } + } + + + /** + * {@link Delegate} for {@link ManagedSet} types. + */ + private static class ManagedSetDelegate extends CollectionDelegate> { + + public ManagedSetDelegate() { + super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class)); + } + } + + + /** + * {@link Delegate} for {@link ManagedMap} types. + */ + private static class ManagedMapDelegate implements Delegate { + + private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class); + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof ManagedMap managedMap) { + return generateManagedMapCode(valueCodeGenerator, managedMap); + } + return null; + } + + private CodeBlock generateManagedMapCode(ValueCodeGenerator valueCodeGenerator, + ManagedMap managedMap) { + if (managedMap.isEmpty()) { + return EMPTY_RESULT; + } + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.ofEntries(", ManagedMap.class); + Iterator> iterator = managedMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + code.add("$T.entry($L,$L)", Map.class, + valueCodeGenerator.generateCode(entry.getKey()), + valueCodeGenerator.generateCode(entry.getValue())); + if (iterator.hasNext()) { + code.add(", "); + } + } + code.add(")"); + return code.build(); + } + } + + + /** + * {@link Delegate} for {@link Map} types. + */ + private static class LinkedHashMapDelegate extends MapDelegate { + + @Override + @Nullable + protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { + GeneratedMethods generatedMethods = valueCodeGenerator.getGeneratedMethods(); + if (map instanceof LinkedHashMap && generatedMethods != null) { + return generateLinkedHashMapCode(valueCodeGenerator, generatedMethods, map); + } + return super.generateMapCode(valueCodeGenerator, map); + } + + private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerator, + GeneratedMethods generatedMethods, Map map) { + + GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> { + method.addAnnotation(AnnotationSpec + .builder(SuppressWarnings.class) + .addMember("value", "{\"rawtypes\", \"unchecked\"}") + .build()); + method.returns(Map.class); + method.addStatement("$T map = new $T($L)", Map.class, + LinkedHashMap.class, map.size()); + map.forEach((key, value) -> method.addStatement("map.put($L, $L)", + valueCodeGenerator.generateCode(key), + valueCodeGenerator.generateCode(value))); + method.addStatement("return map"); + }); + return CodeBlock.of("$L()", generatedMethod.getName()); + } + } + + + /** + * {@link Delegate} for {@link BeanReference} types. + */ + private static class BeanReferenceDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof RuntimeBeanReference runtimeBeanReference && + runtimeBeanReference.getBeanType() != null) { + return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class, + runtimeBeanReference.getBeanType()); + } + else if (value instanceof BeanReference beanReference) { + return CodeBlock.of("new $T($S)", RuntimeBeanReference.class, + beanReference.getBeanName()); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link TypedStringValue} types. + */ + private static class TypedStringValueDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof TypedStringValue typedStringValue) { + return generateTypeStringValueCode(valueCodeGenerator, typedStringValue); + } + return null; + } + + private CodeBlock generateTypeStringValueCode(ValueCodeGenerator valueCodeGenerator, TypedStringValue typedStringValue) { + String value = typedStringValue.getValue(); + if (typedStringValue.hasTargetType()) { + return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value, + valueCodeGenerator.generateCode(typedStringValue.getTargetType())); + } + return valueCodeGenerator.generateCode(value); + } + } +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java index 265e939e01c0..a1992cb88760 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,17 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.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; @@ -209,12 +213,13 @@ private T invokeBeanSupplier(Executable executable, ThrowingSupplier beanSupp if (!(executable instanceof Method method)) { return beanSupplier.get(); } + Method priorInvokedFactoryMethod = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); try { SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(method); return beanSupplier.get(); } finally { - SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(null); + SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(priorInvokedFactoryMethod); } } @@ -247,7 +252,7 @@ private AutowiredArguments resolveArguments(RegisteredBean registeredBean, Execu Assert.isTrue(this.shortcuts == null || this.shortcuts.length == resolved.length, () -> "'shortcuts' must contain " + resolved.length + " elements"); - ConstructorArgumentValues argumentValues = resolveArgumentValues(registeredBean); + ValueHolder[] argumentValues = resolveArgumentValues(registeredBean, executable); Set autowiredBeanNames = new LinkedHashSet<>(resolved.length * 2); for (int i = startIndex; i < parameterCount; i++) { MethodParameter parameter = getMethodParameter(executable, i); @@ -256,8 +261,9 @@ private AutowiredArguments resolveArguments(RegisteredBean registeredBean, Execu if (shortcut != null) { descriptor = new ShortcutDependencyDescriptor(descriptor, shortcut); } - ValueHolder argumentValue = argumentValues.getIndexedArgumentValue(i, null); - resolved[i - startIndex] = resolveArgument(registeredBean, descriptor, argumentValue, autowiredBeanNames); + ValueHolder argumentValue = argumentValues[i]; + resolved[i - startIndex] = resolveAutowiredArgument( + registeredBean, descriptor, argumentValue, autowiredBeanNames); } registerDependentBeans(registeredBean.getBeanFactory(), registeredBean.getBeanName(), autowiredBeanNames); @@ -274,22 +280,44 @@ private MethodParameter getMethodParameter(Executable executable, int index) { throw new IllegalStateException("Unsupported executable: " + executable.getClass().getName()); } - private ConstructorArgumentValues resolveArgumentValues(RegisteredBean registeredBean) { - ConstructorArgumentValues resolved = new ConstructorArgumentValues(); + private ValueHolder[] resolveArgumentValues(RegisteredBean registeredBean, Executable executable) { + Parameter[] parameters = executable.getParameters(); + ValueHolder[] resolved = new ValueHolder[parameters.length]; RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); if (beanDefinition.hasConstructorArgumentValues() && registeredBean.getBeanFactory() instanceof AbstractAutowireCapableBeanFactory beanFactory) { BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver( beanFactory, registeredBean.getBeanName(), beanDefinition, beanFactory.getTypeConverter()); - ConstructorArgumentValues values = beanDefinition.getConstructorArgumentValues(); - values.getIndexedArgumentValues().forEach((index, valueHolder) -> { - ValueHolder resolvedValue = resolveArgumentValue(valueResolver, valueHolder); - resolved.addIndexedArgumentValue(index, resolvedValue); - }); + ConstructorArgumentValues values = resolveConstructorArguments( + valueResolver, beanDefinition.getConstructorArgumentValues()); + Set usedValueHolders = new HashSet<>(parameters.length); + for (int i = 0; i < parameters.length; i++) { + Class parameterType = parameters[i].getType(); + String parameterName = (parameters[i].isNamePresent() ? parameters[i].getName() : null); + ValueHolder valueHolder = values.getArgumentValue( + i, parameterType, parameterName, usedValueHolders); + if (valueHolder != null) { + resolved[i] = valueHolder; + usedValueHolders.add(valueHolder); + } + } } return resolved; } + private ConstructorArgumentValues resolveConstructorArguments( + BeanDefinitionValueResolver valueResolver, ConstructorArgumentValues constructorArguments) { + + ConstructorArgumentValues resolvedConstructorArguments = new ConstructorArgumentValues(); + for (Map.Entry entry : constructorArguments.getIndexedArgumentValues().entrySet()) { + resolvedConstructorArguments.addIndexedArgumentValue(entry.getKey(), resolveArgumentValue(valueResolver, entry.getValue())); + } + for (ConstructorArgumentValues.ValueHolder valueHolder : constructorArguments.getGenericArgumentValues()) { + resolvedConstructorArguments.addGenericArgumentValue(resolveArgumentValue(valueResolver, valueHolder)); + } + return resolvedConstructorArguments; + } + private ValueHolder resolveArgumentValue(BeanDefinitionValueResolver resolver, ValueHolder valueHolder) { if (valueHolder.isConverted()) { return valueHolder; @@ -301,7 +329,7 @@ private ValueHolder resolveArgumentValue(BeanDefinitionValueResolver resolver, V } @Nullable - private Object resolveArgument(RegisteredBean registeredBean, DependencyDescriptor descriptor, + private Object resolveAutowiredArgument(RegisteredBean registeredBean, DependencyDescriptor descriptor, @Nullable ValueHolder argumentValue, Set autowiredBeanNames) { TypeConverter typeConverter = registeredBean.getBeanFactory().getTypeConverter(); @@ -345,8 +373,7 @@ private Object instantiate(Constructor constructor, Object[] args) throws Exc Object enclosingInstance = createInstance(declaringClass.getEnclosingClass()); args = ObjectUtils.addObjectToArray(args, enclosingInstance, 0); } - ReflectionUtils.makeAccessible(constructor); - return constructor.newInstance(args); + return BeanUtils.instantiateClass(constructor, args); } private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, Object[] args) throws Exception { @@ -384,7 +411,7 @@ private static String toCommaSeparatedNames(Class... parameterTypes) { /** * Performs lookup of the {@link Executable}. */ - static abstract class ExecutableLookup { + abstract static class ExecutableLookup { abstract Executable get(RegisteredBean registeredBean); } 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 4febbdd8bcb1..42a16c15238a 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.function.UnaryOperator; import org.springframework.aot.generate.GenerationContext; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -82,4 +83,31 @@ public void applyTo(GenerationContext generationContext, }; } + /** + * Create a contribution that applies the contribution of the first contribution + * followed by the second contribution. Any contribution can be {@code null} to be + * ignored and the concatenated contribution is {@code null} if both inputs are + * {@code null}. + * @param a the first contribution + * @param b the second contribution + * @return the concatenation of the two contributions, or {@code null} if + * they are both {@code null}. + * @since 6.1 + */ + @Nullable + static BeanRegistrationAotContribution concat(@Nullable BeanRegistrationAotContribution a, + @Nullable BeanRegistrationAotContribution b) { + + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return (generationContext, beanRegistrationCode) -> { + a.applyTo(generationContext, beanRegistrationCode); + b.applyTo(generationContext, beanRegistrationCode); + }; + } + } 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 d7f02a43e5ec..db1bd2e81556 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ package org.springframework.beans.factory.aot; -import java.lang.reflect.Executable; import java.util.List; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; @@ -31,9 +31,19 @@ /** * Generate the various fragments of code needed to register a bean. - * + *

+ * 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). * @author Phillip Webb + * @author Stephane Nicoll * @since 6.0 + * @see BeanRegistrationCodeFragmentsDecorator + * @see BeanRegistrationAotContribution#withCustomCodeFragments(UnaryOperator) */ public interface BeanRegistrationCodeFragments { @@ -50,16 +60,19 @@ public interface BeanRegistrationCodeFragments { /** * Return the target for the registration. Used to determine where to write - * the code. + * the code. This should take into account visibility issue, such as + * package access of an element of the bean to register. * @param registeredBean the registered bean - * @param constructorOrFactoryMethod the constructor or factory method * @return the target {@link ClassName} */ - ClassName getTarget(RegisteredBean registeredBean, - Executable constructorOrFactoryMethod); + ClassName getTarget(RegisteredBean registeredBean); /** * Generate the code that defines the new bean definition instance. + *

+ * 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 * @param beanType the bean type * @param beanRegistrationCode the bean registration code @@ -81,6 +94,11 @@ CodeBlock generateSetBeanDefinitionPropertiesCode( /** * Generate the code that sets the instance supplier on the bean definition. + *

+ * 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 @@ -96,15 +114,13 @@ CodeBlock generateSetBeanInstanceSupplierCode( * Generate the instance supplier code. * @param generationContext the generation context * @param beanRegistrationCode the bean registration code - * @param constructorOrFactoryMethod the constructor or factory method for - * the bean * @param allowDirectSupplierShortcut if direct suppliers may be used rather * than always needing an {@link InstanceSupplier} * @return the generated code */ CodeBlock generateInstanceSupplierCode( GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, - Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut); + boolean allowDirectSupplierShortcut); /** * Generate the return statement. 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 e4ff961262e1..4820770a1022 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.beans.factory.aot; -import java.lang.reflect.Executable; import java.util.List; import java.util.function.Predicate; import java.util.function.UnaryOperator; @@ -51,48 +50,47 @@ protected BeanRegistrationCodeFragmentsDecorator(BeanRegistrationCodeFragments d } @Override - public ClassName getTarget(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { - return this.delegate.getTarget(registeredBean, constructorOrFactoryMethod); + public ClassName getTarget(RegisteredBean registeredBean) { + return this.delegate.getTarget(registeredBean); } @Override 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, Executable constructorOrFactoryMethod, + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { return this.delegate.generateInstanceSupplierCode(generationContext, - beanRegistrationCode, constructorOrFactoryMethod, allowDirectSupplierShortcut); + 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 3547378b0673..98564d4852e7 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.beans.factory.aot; -import java.lang.reflect.Executable; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; @@ -47,19 +46,15 @@ class BeanRegistrationCodeGenerator implements BeanRegistrationCode { private final RegisteredBean registeredBean; - private final Executable constructorOrFactoryMethod; - private final BeanRegistrationCodeFragments codeFragments; BeanRegistrationCodeGenerator(ClassName className, GeneratedMethods generatedMethods, - RegisteredBean registeredBean, Executable constructorOrFactoryMethod, - BeanRegistrationCodeFragments codeFragments) { + RegisteredBean registeredBean, BeanRegistrationCodeFragments codeFragments) { this.className = className; this.generatedMethods = generatedMethods; this.registeredBean = registeredBean; - this.constructorOrFactoryMethod = constructorOrFactoryMethod; this.codeFragments = codeFragments; } @@ -87,8 +82,7 @@ CodeBlock generateCode(GenerationContext generationContext) { generationContext, this, this.registeredBean.getMergedBeanDefinition(), REJECT_ALL_ATTRIBUTES_FILTER)); CodeBlock instanceSupplierCode = this.codeFragments.generateInstanceSupplierCode( - generationContext, this, this.constructorOrFactoryMethod, - this.instancePostProcessors.isEmpty()); + generationContext, this, this.instancePostProcessors.isEmpty()); code.add(this.codeFragments.generateSetBeanInstanceSupplierCode(generationContext, this, instanceSupplierCode, this.instancePostProcessors)); code.add(this.codeFragments.generateReturnCode(generationContext, this)); 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 index ffd3f99c9c7d..cc23256f6c44 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,10 @@ /** * 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 + * @param beanName the name of the registered bean + * @param beanClass the type of the registered bean */ 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 d93e9507d68e..5960d80952d1 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 @@ -16,8 +16,6 @@ package org.springframework.beans.factory.aot; -import java.lang.reflect.GenericArrayType; -import java.lang.reflect.Type; import java.util.Map; import javax.lang.model.element.Modifier; @@ -32,11 +30,10 @@ import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.core.ResolvableType; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; -import org.springframework.util.ReflectionUtils; +import org.springframework.util.ClassUtils; /** * AOT contribution from a {@link BeanRegistrationsAotProcessor} used to @@ -117,25 +114,24 @@ private void generateRegisterHints(RuntimeHints runtimeHints, Map { ReflectionHints hints = runtimeHints.reflection(); Class beanClass = beanRegistrationKey.beanClass(); - hints.registerType(beanClass, MemberCategory.INTROSPECT_DECLARED_METHODS); - // Workaround for https://github.com/oracle/graal/issues/6510 - if (beanClass.isRecord()) { - hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_METHODS); - } - // Workaround for https://github.com/oracle/graal/issues/6529 - ReflectionUtils.doWithMethods(beanClass, method -> { - for (Type type : method.getGenericParameterTypes()) { - if (type instanceof GenericArrayType) { - Class clazz = ResolvableType.forType(type).resolve(); - if (clazz != null) { - hints.registerType(clazz); - } - } - } - }); + hints.registerType(beanClass, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS); + introspectPublicMethodsOnAllInterfaces(hints, beanClass); }); } + private void introspectPublicMethodsOnAllInterfaces(ReflectionHints hints, Class type) { + Class currentClass = type; + while (currentClass != null && currentClass != Object.class) { + for (Class interfaceType : currentClass.getInterfaces()) { + if (!ClassUtils.isJavaLanguageInterface(interfaceType)) { + hints.registerType(interfaceType, MemberCategory.INTROSPECT_PUBLIC_METHODS); + introspectPublicMethodsOnAllInterfaces(hints, interfaceType); + } + } + currentClass = currentClass.getSuperclass(); + } + } + /** * Gather the necessary information to register a particular bean. * @param methodGenerator the {@link BeanDefinitionMethodGenerator} to use 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 new file mode 100644 index 000000000000..7809ba2ca17f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.aot; + +import java.lang.reflect.AnnotatedElement; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.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.javapoet.TypeSpec; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Helper class to register warnings that the compiler may trigger on + * generated code. + * + * @author Stephane Nicoll + * @since 6.1 + * @see SuppressWarnings + */ +public class CodeWarnings { + + private final Set warnings = new LinkedHashSet<>(); + + + /** + * Register a warning to be included for this block. Does nothing if + * the warning is already registered. + * @param warning the warning to register, if it hasn't been already + */ + public void register(String warning) { + this.warnings.add(warning); + } + + /** + * Detect the presence of {@link Deprecated} on the specified elements. + * @param elements the elements to check + * @return {@code this} instance + */ + public CodeWarnings detectDeprecation(AnnotatedElement... elements) { + for (AnnotatedElement element : elements) { + registerDeprecationIfNecessary(element); + } + return this; + } + + /** + * Detect the presence of {@link Deprecated} on the specified elements. + * @param elements the elements to check + * @return {@code this} instance + */ + public CodeWarnings detectDeprecation(Stream elements) { + elements.forEach(element -> register(element.getAnnotation(Deprecated.class))); + return this; + } + + /** + * Detect the presence of {@link Deprecated} on the signature of the + * specified {@link ResolvableType}. + * @param resolvableType a type signature + * @return {@code this} instance + * @since 6.1.8 + */ + public CodeWarnings detectDeprecation(ResolvableType resolvableType) { + if (ResolvableType.NONE.equals(resolvableType)) { + return this; + } + Class type = ClassUtils.getUserClass(resolvableType.toClass()); + detectDeprecation(type); + if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) { + for (ResolvableType generic : resolvableType.getGenerics()) { + detectDeprecation(generic); + } + } + return this; + } + + /** + * Include {@link SuppressWarnings} on the specified method if necessary. + * @param method the method to update + */ + public void suppress(MethodSpec.Builder method) { + 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); + } + } + + /** + * Return the currently registered warnings. + * @return the warnings + */ + 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()) { + register("removal"); + } + else { + register("deprecation"); + } + } + } + + private CodeBlock generateValueCode() { + if (this.warnings.size() == 1) { + return CodeBlock.of("$S", this.warnings.iterator().next()); + } + CodeBlock values = CodeBlock.join(this.warnings.stream() + .map(warning -> CodeBlock.of("$S", warning)).toList(), ", "); + return CodeBlock.of("{ $L }", values); + } + + @Override + public String toString() { + return new StringJoiner(", ", CodeWarnings.class.getSimpleName(), "") + .add(this.warnings.toString()) + .toString(); + } + +} 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 d5281ef6caf3..57c792af1768 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,20 +17,24 @@ package org.springframework.beans.factory.aot; import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; import java.lang.reflect.Modifier; import java.util.List; import java.util.function.Predicate; +import java.util.function.Supplier; import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.aot.AotServices.Loader; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.InstanceSupplier; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.ResolvableType; import org.springframework.javapoet.ClassName; @@ -39,21 +43,26 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.function.SingletonSupplier; /** - * Internal {@link BeanRegistrationCodeFragments} implementation used by - * default. + * Internal {@link BeanRegistrationCodeFragments} implementation used by default. * * @author Phillip Webb + * @author Stephane Nicoll */ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragments { + private static final ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + private final BeanRegistrationsCode beanRegistrationsCode; private final RegisteredBean registeredBean; private final BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory; + private final Supplier instantiationDescriptor; + DefaultBeanRegistrationCodeFragments(BeanRegistrationsCode beanRegistrationsCode, RegisteredBean registeredBean, @@ -62,37 +71,40 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme this.beanRegistrationsCode = beanRegistrationsCode; this.registeredBean = registeredBean; this.beanDefinitionMethodGeneratorFactory = beanDefinitionMethodGeneratorFactory; + this.instantiationDescriptor = SingletonSupplier.of(registeredBean::resolveInstantiationDescriptor); } @Override - public ClassName getTarget(RegisteredBean registeredBean, - Executable constructorOrFactoryMethod) { - - Class target = extractDeclaringClass(registeredBean.getBeanType(), constructorOrFactoryMethod); + 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"); + } + Class target = extractDeclaringClass(registeredBean, this.instantiationDescriptor.get()); while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) { RegisteredBean parent = registeredBean.getParent(); Assert.state(parent != null, "No parent available for inner bean"); target = parent.getBeanClass(); } - return ClassName.get(target); + return (target.isArray() ? ClassName.get(target.getComponentType()) : ClassName.get(target)); } - private Class extractDeclaringClass(ResolvableType beanType, Executable executable) { - Class declaringClass = ClassUtils.getUserClass(executable.getDeclaringClass()); - if (executable instanceof Constructor - && AccessControl.forMember(executable).isPublic() - && FactoryBean.class.isAssignableFrom(declaringClass)) { - return extractTargetClassFromFactoryBean(declaringClass, beanType); + private Class extractDeclaringClass(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) { + Class declaringClass = ClassUtils.getUserClass(instantiationDescriptor.targetClass()); + if (instantiationDescriptor.executable() instanceof Constructor ctor && + AccessControl.forMember(ctor).isPublic() && FactoryBean.class.isAssignableFrom(declaringClass)) { + return extractTargetClassFromFactoryBean(declaringClass, registeredBean.getBeanType()); } - return executable.getDeclaringClass(); + return declaringClass; } /** * Extract the target class of a public {@link FactoryBean} based on its * constructor. If the implementation does not resolve the target class - * because it itself uses a generic, attempt to extract it from the - * bean type. + * because it itself uses a generic, attempt to extract it from the bean type. * @param factoryBeanType the factory bean type * @param beanType the bean type * @return the target class to use @@ -113,17 +125,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(); } @@ -139,17 +149,16 @@ private CodeBlock generateBeanClassCode(String targetPackage, Class beanClass private CodeBlock generateBeanTypeCode(ResolvableType beanType) { if (!beanType.hasGenerics()) { - return CodeBlock.of("$T.class", ClassUtils.getUserClass(beanType.toClass())); + return valueCodeGenerator.generateCode(ClassUtils.getUserClass(beanType.toClass())); } - return ResolvableTypeCodeGenerator.generateCode(beanType); + return valueCodeGenerator.generateCode(beanType); } private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class beanClass) { if (beanType.hasGenerics()) { return true; } - if (beanClass != null - && this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) { + if (beanClass != null && this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) { return true; } return (beanClass != null && !beanType.toClass().equals(beanClass)); @@ -157,21 +166,21 @@ private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class @Override public CodeBlock generateSetBeanDefinitionPropertiesCode( - GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, - Predicate attributeFilter) { + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { + + Loader loader = AotServices.factories(this.registeredBean.getBeanFactory().getBeanClassLoader()); + List additionalDelegates = loader.load(Delegate.class).asList(); return new BeanDefinitionPropertiesCodeGenerator( generationContext.getRuntimeHints(), attributeFilter, - beanRegistrationCode.getMethods(), + beanRegistrationCode.getMethods(), additionalDelegates, (name, value) -> generateValueCode(generationContext, name, value)) .generateCode(beanDefinition); } @Nullable - protected CodeBlock generateValueCode(GenerationContext generationContext, - String name, Object value) { - + protected CodeBlock generateValueCode(GenerationContext generationContext, String name, Object value) { RegisteredBean innerRegisteredBean = getInnerRegisteredBean(value); if (innerRegisteredBean != null) { BeanDefinitionMethodGenerator methodGenerator = this.beanDefinitionMethodGeneratorFactory @@ -197,9 +206,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()) { @@ -219,22 +227,30 @@ public CodeBlock generateSetBeanInstanceSupplierCode( } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, - Executable constructorOrFactoryMethod, 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()); + } return new InstanceSupplierCodeGenerator(generationContext, beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut) - .generateCode(this.registeredBean, constructorOrFactoryMethod); + .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); return code.build(); } + private boolean hasInstanceSupplier() { + 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 bd1ff37f77f2..acc796df5827 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,16 @@ 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; import java.util.function.Consumer; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; + import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.AccessControl.Visibility; import org.springframework.aot.generate.GeneratedMethod; @@ -31,8 +38,17 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.InstanceSupplier; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; +import org.springframework.core.KotlinDetector; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; @@ -43,9 +59,10 @@ import org.springframework.util.function.ThrowingSupplier; /** - * Internal code generator to create an {@link InstanceSupplier}, usually in + * Default code generator to create an {@link InstanceSupplier}, usually in * the form of a {@link BeanInstanceSupplier} that retains the executable - * that is used to instantiate the bean. + * that is used to instantiate the bean. Takes care of registering the + * necessary hints if reflection or a JDK proxy is required. * *

Generated code is usually a method reference that generates the * {@link BeanInstanceSupplier}, but some shortcut can be used as well such as: @@ -56,9 +73,11 @@ * @author Phillip Webb * @author Stephane Nicoll * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 6.0 + * @see BeanRegistrationCodeFragments */ -class InstanceSupplierCodeGenerator { +public class InstanceSupplierCodeGenerator { private static final String REGISTERED_BEAN_PARAMETER_NAME = "registeredBean"; @@ -80,7 +99,15 @@ class InstanceSupplierCodeGenerator { private final boolean allowDirectSupplierShortcut; - InstanceSupplierCodeGenerator(GenerationContext generationContext, + /** + * Create a new instance. + * @param generationContext the generation context + * @param className the class name of the bean to instantiate + * @param generatedMethods the generated methods + * @param allowDirectSupplierShortcut whether a direct supplier may be used rather + * than always needing an {@link InstanceSupplier} + */ + public InstanceSupplierCodeGenerator(GenerationContext generationContext, ClassName className, GeneratedMethods generatedMethods, boolean allowDirectSupplierShortcut) { this.generationContext = generationContext; @@ -89,18 +116,52 @@ class InstanceSupplierCodeGenerator { this.allowDirectSupplierShortcut = allowDirectSupplierShortcut; } + /** + * Generate the instance supplier code. + * @param registeredBean the bean to handle + * @param constructorOrFactoryMethod the executable to use to create the bean + * @return the generated code + * @deprecated in favor of {@link #generateCode(RegisteredBean, InstantiationDescriptor)} + */ + @Deprecated(since = "6.1.7") + public CodeBlock generateCode(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { + return generateCode(registeredBean, new InstantiationDescriptor( + constructorOrFactoryMethod, constructorOrFactoryMethod.getDeclaringClass())); + } - CodeBlock generateCode(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { + /** + * Generate the instance supplier code. + * @param registeredBean the bean to handle + * @param instantiationDescriptor the executable to use to create the bean + * @return the generated code + * @since 6.1.7 + */ + public CodeBlock generateCode(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) { + Executable constructorOrFactoryMethod = instantiationDescriptor.executable(); + registerRuntimeHintsIfNecessary(registeredBean, constructorOrFactoryMethod); if (constructorOrFactoryMethod instanceof Constructor constructor) { return generateCodeForConstructor(registeredBean, constructor); } if (constructorOrFactoryMethod instanceof Method method) { - return generateCodeForFactoryMethod(registeredBean, method); + return generateCodeForFactoryMethod(registeredBean, method, instantiationDescriptor.targetClass()); } throw new IllegalStateException( "No suitable executor found for " + registeredBean.getBeanName()); } + 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); + } + } + } + private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Constructor constructor) { String beanName = registeredBean.getBeanName(); Class beanClass = registeredBean.getBeanClass(); @@ -108,11 +169,16 @@ private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Cons boolean dependsOnBean = ClassUtils.isInnerClass(declaringClass); Visibility accessVisibility = getAccessVisibility(registeredBean, constructor); - if (accessVisibility != Visibility.PRIVATE) { + if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) { + return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, + dependsOnBean, hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + } + else if (accessVisibility != Visibility.PRIVATE) { return generateCodeForAccessibleConstructor(beanName, beanClass, constructor, dependsOnBean, declaringClass); } - return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean); + return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean, + hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE)); } private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class beanClass, @@ -137,15 +203,18 @@ private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class return generateReturnStatement(generatedMethod); } - private CodeBlock generateCodeForInaccessibleConstructor(String beanName, - Class beanClass, Constructor constructor, boolean dependsOnBean) { + private CodeBlock generateCodeForInaccessibleConstructor(String beanName, Class beanClass, + Constructor constructor, boolean dependsOnBean, Consumer hints) { - this.generationContext.getRuntimeHints().reflection() - .registerConstructor(constructor, ExecutableMode.INVOKE); + CodeWarnings codeWarnings = new CodeWarnings(); + codeWarnings.detectDeprecation(beanClass, 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.addModifiers(PRIVATE_STATIC); + codeWarnings.suppress(method); method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); int parameterOffset = (!dependsOnBean) ? 0 : 1; method.addStatement(generateResolverForConstructor(beanClass, constructor, parameterOffset)); @@ -158,8 +227,12 @@ private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, String beanName, Class beanClass, Constructor constructor, Class declaringClass, boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) { + CodeWarnings codeWarnings = new CodeWarnings(); + codeWarnings.detectDeprecation(beanClass, constructor, declaringClass) + .detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType)); method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); method.addModifiers(modifiers); + codeWarnings.suppress(method); method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); int parameterOffset = (!dependsOnBean) ? 0 : 1; @@ -196,21 +269,21 @@ private CodeBlock generateNewInstanceCodeForConstructor(boolean dependsOnBean, declaringClass.getSimpleName(), args); } - private CodeBlock generateCodeForFactoryMethod(RegisteredBean registeredBean, Method factoryMethod) { + private CodeBlock generateCodeForFactoryMethod(RegisteredBean registeredBean, Method factoryMethod, Class targetClass) { String beanName = registeredBean.getBeanName(); - Class declaringClass = ClassUtils.getUserClass(factoryMethod.getDeclaringClass()); + Class targetClassToUse = ClassUtils.getUserClass(targetClass); boolean dependsOnBean = !Modifier.isStatic(factoryMethod.getModifiers()); Visibility accessVisibility = getAccessVisibility(registeredBean, factoryMethod); if (accessVisibility != Visibility.PRIVATE) { return generateCodeForAccessibleFactoryMethod( - beanName, factoryMethod, declaringClass, dependsOnBean); + beanName, factoryMethod, targetClassToUse, dependsOnBean); } - return generateCodeForInaccessibleFactoryMethod(beanName, factoryMethod, declaringClass); + return generateCodeForInaccessibleFactoryMethod(beanName, factoryMethod, targetClassToUse); } private CodeBlock generateCodeForAccessibleFactoryMethod(String beanName, - Method factoryMethod, Class declaringClass, boolean dependsOnBean) { + Method factoryMethod, Class targetClass, boolean dependsOnBean) { this.generationContext.getRuntimeHints().reflection().registerMethod( factoryMethod, ExecutableMode.INTROSPECT); @@ -219,20 +292,20 @@ private CodeBlock generateCodeForAccessibleFactoryMethod(String beanName, Class suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType()); CodeBlock.Builder code = CodeBlock.builder(); code.add("$T.<$T>forFactoryMethod($T.class, $S)", BeanInstanceSupplier.class, - suppliedType, declaringClass, factoryMethod.getName()); + suppliedType, targetClass, factoryMethod.getName()); code.add(".withGenerator(($L) -> $T.$L())", REGISTERED_BEAN_PARAMETER_NAME, - declaringClass, factoryMethod.getName()); + targetClass, factoryMethod.getName()); return code.build(); } GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> buildGetInstanceMethodForFactoryMethod(method, beanName, factoryMethod, - declaringClass, dependsOnBean, PRIVATE_STATIC)); + targetClass, dependsOnBean, PRIVATE_STATIC)); return generateReturnStatement(getInstanceMethod); } private CodeBlock generateCodeForInaccessibleFactoryMethod( - String beanName, Method factoryMethod, Class declaringClass) { + String beanName, Method factoryMethod, Class targetClass) { this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE); GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> { @@ -241,59 +314,63 @@ private CodeBlock generateCodeForInaccessibleFactoryMethod( method.addModifiers(PRIVATE_STATIC); method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType)); method.addStatement(generateInstanceSupplierForFactoryMethod( - factoryMethod, suppliedType, declaringClass, factoryMethod.getName())); + factoryMethod, suppliedType, targetClass, factoryMethod.getName())); }); return generateReturnStatement(getInstanceMethod); } private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method, - String beanName, Method factoryMethod, Class declaringClass, + String beanName, Method factoryMethod, Class targetClass, boolean dependsOnBean, 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) + .detectDeprecation(Arrays.stream(factoryMethod.getParameters()).map(Parameter::getType)); method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); method.addModifiers(modifiers); + codeWarnings.suppress(method); method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType)); CodeBlock.Builder code = CodeBlock.builder(); code.add(generateInstanceSupplierForFactoryMethod( - factoryMethod, suppliedType, declaringClass, factoryMethodName)); + factoryMethod, suppliedType, targetClass, factoryMethodName)); boolean hasArguments = factoryMethod.getParameterCount() > 0; CodeBlock arguments = hasArguments ? - new AutowiredArgumentsCodeGenerator(declaringClass, factoryMethod) + new AutowiredArgumentsCodeGenerator(targetClass, factoryMethod) .generateCode(factoryMethod.getParameterTypes()) : NO_ARGS; CodeBlock newInstance = generateNewInstanceCodeForMethod( - dependsOnBean, declaringClass, factoryMethodName, arguments); + dependsOnBean, targetClass, factoryMethodName, arguments); code.add(generateWithGeneratorCode(hasArguments, newInstance)); method.addStatement(code.build()); } private CodeBlock generateInstanceSupplierForFactoryMethod(Method factoryMethod, - Class suppliedType, Class declaringClass, String factoryMethodName) { + Class suppliedType, Class targetClass, String factoryMethodName) { if (factoryMethod.getParameterCount() == 0) { return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S)", - BeanInstanceSupplier.class, suppliedType, declaringClass, factoryMethodName); + BeanInstanceSupplier.class, suppliedType, targetClass, factoryMethodName); } CodeBlock parameterTypes = generateParameterTypesCode(factoryMethod.getParameterTypes(), 0); return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S, $L)", - BeanInstanceSupplier.class, suppliedType, declaringClass, factoryMethodName, parameterTypes); + BeanInstanceSupplier.class, suppliedType, targetClass, factoryMethodName, parameterTypes); } private CodeBlock generateNewInstanceCodeForMethod(boolean dependsOnBean, - Class declaringClass, String factoryMethodName, CodeBlock args) { + Class targetClass, String factoryMethodName, CodeBlock args) { if (!dependsOnBean) { - return CodeBlock.of("$T.$L($L)", declaringClass, factoryMethodName, args); + return CodeBlock.of("$T.$L($L)", targetClass, factoryMethodName, args); } return CodeBlock.of("$L.getBeanFactory().getBean($T.class).$L($L)", - REGISTERED_BEAN_PARAMETER_NAME, declaringClass, factoryMethodName, args); + REGISTERED_BEAN_PARAMETER_NAME, targetClass, factoryMethodName, args); } private CodeBlock generateReturnStatement(GeneratedMethod generatedMethod) { @@ -338,4 +415,61 @@ private boolean isThrowingCheckedException(Executable executable) { .anyMatch(Exception.class::isAssignableFrom); } + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + public static boolean hasConstructorWithOptionalParameter(Class beanClass) { + if (KotlinDetector.isKotlinType(beanClass)) { + KClass kClass = JvmClassMappingKt.getKotlinClass(beanClass); + for (KFunction constructor : kClass.getConstructors()) { + for (KParameter parameter : constructor.getParameters()) { + if (parameter.isOptional()) { + return true; + } + } + } + } + return false; + } + + } + + + private static class ProxyRuntimeHintsRegistrar { + + private final AutowireCandidateResolver candidateResolver; + + public ProxyRuntimeHintsRegistrar(AutowireCandidateResolver candidateResolver) { + this.candidateResolver = candidateResolver; + } + + public void registerRuntimeHints(RuntimeHints runtimeHints, Method method) { + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + MethodParameter methodParam = new MethodParameter(method, 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)) { + runtimeHints.proxies().registerJdkProxy(proxyType.getInterfaces()); + } + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java deleted file mode 100644 index e7b715dd006c..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.beans.factory.aot; - -import java.util.Arrays; - -import org.springframework.core.ResolvableType; -import org.springframework.javapoet.CodeBlock; -import org.springframework.util.ClassUtils; - -/** - * Internal code generator used to support {@link ResolvableType}. - * - * @author Stephane Nicoll - * @author Phillip Webb - * @since 6.0 - */ -final class ResolvableTypeCodeGenerator { - - - private ResolvableTypeCodeGenerator() { - } - - - public static CodeBlock generateCode(ResolvableType resolvableType) { - return generateCode(resolvableType, false); - } - - private static CodeBlock generateCode(ResolvableType resolvableType, boolean allowClassResult) { - if (ResolvableType.NONE.equals(resolvableType)) { - return CodeBlock.of("$T.NONE", ResolvableType.class); - } - Class type = ClassUtils.getUserClass(resolvableType.toClass()); - if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) { - return generateCodeWithGenerics(resolvableType, type); - } - if (allowClassResult) { - return CodeBlock.of("$T.class", type); - } - return CodeBlock.of("$T.forClass($T.class)", ResolvableType.class, type); - } - - private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class type) { - ResolvableType[] generics = target.getGenerics(); - boolean hasNoNestedGenerics = Arrays.stream(generics).noneMatch(ResolvableType::hasGenerics); - CodeBlock.Builder code = CodeBlock.builder(); - code.add("$T.forClassWithGenerics($T.class", ResolvableType.class, type); - for (ResolvableType generic : generics) { - code.add(", $L", generateCode(generic, hasNoNestedGenerics)); - } - code.add(")"); - return code.build(); - } - -} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java index fb6e92b1fafe..efda24780820 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 @@ -64,7 +64,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Constant that indicates no externally defined autowiring. Note that * BeanFactoryAware etc and annotation-driven injection will still be applied. - * @see #createBean * @see #autowire * @see #autowireBeanProperties */ @@ -73,7 +72,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Constant that indicates autowiring bean properties by name * (applying to all bean property setters). - * @see #createBean * @see #autowire * @see #autowireBeanProperties */ @@ -82,7 +80,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Constant that indicates autowiring bean properties by type * (applying to all bean property setters). - * @see #createBean * @see #autowire * @see #autowireBeanProperties */ @@ -91,7 +88,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Constant that indicates autowiring the greediest constructor that * can be satisfied (involves resolving the appropriate constructor). - * @see #createBean * @see #autowire */ int AUTOWIRE_CONSTRUCTOR = 3; @@ -99,7 +95,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Constant that indicates determining an appropriate autowire strategy * through introspection of the bean class. - * @see #createBean * @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. @@ -192,7 +187,9 @@ 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(since = "6.1") Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException; /** @@ -300,7 +297,10 @@ void autowireBeanProperties(Object existingBean, int autowireMode, boolean depen * @throws BeansException if any post-processing failed * @see BeanPostProcessor#postProcessBeforeInitialization * @see #ORIGINAL_INSTANCE_SUFFIX + * @deprecated as of 6.1, in favor of implicit post-processing through + * {@link #initializeBean(Object, String)} */ + @Deprecated(since = "6.1") Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException; @@ -317,12 +317,15 @@ Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String b * @throws BeansException if any post-processing failed * @see BeanPostProcessor#postProcessAfterInitialization * @see #ORIGINAL_INSTANCE_SUFFIX + * @deprecated as of 6.1, in favor of implicit post-processing through + * {@link #initializeBean(Object, String)} */ + @Deprecated(since = "6.1") Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException; /** - * Destroy the given bean instance (typically coming from {@link #createBean}), + * Destroy the given bean instance (typically coming from {@link #createBean(Class)}), * applying the {@link org.springframework.beans.factory.DisposableBean} contract as well as * registered {@link DestructionAwareBeanPostProcessor DestructionAwareBeanPostProcessors}. *

Any exception that arises during destruction should be caught 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 36a56cae9d94..9b76f819fce2 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 @@ -173,10 +173,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = this.beanDefinition.hashCode(); - hashCode = 29 * hashCode + this.beanName.hashCode(); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.aliases); - return hashCode; + return ObjectUtils.nullSafeHash(this.beanDefinition, this.beanName, this.aliases); } } 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 7c240653ebb0..175f5a4c0ba6 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 @@ -606,7 +606,7 @@ private boolean contentEquals(ValueHolder other) { * same content to reside in the same Set. */ private int contentHashCode() { - return ObjectUtils.nullSafeHashCode(this.value) * 29 + ObjectUtils.nullSafeHashCode(this.type); + return ObjectUtils.nullSafeHash(this.value, this.type); } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index 4825563735ff..499628a11386 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -375,6 +375,16 @@ public Class getDependencyType() { } } + /** + * Determine whether this dependency supports lazy resolution, + * e.g. through extra proxying. The default is {@code true}. + * @since 6.1.2 + * @see org.springframework.beans.factory.support.AutowireCandidateResolver#getLazyResolutionProxyIfNecessary + */ + public boolean supportsLazyResolution() { + return true; + } + @Override public boolean equals(@Nullable Object other) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java index 9f7657f196c4..86b6ccc23471 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 @@ -226,6 +226,7 @@ public Object getObject() throws IllegalAccessException { } @Override + @Nullable public Class getObjectType() { return (this.fieldObject != null ? this.fieldObject.getType() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java index 3ff39d81e5e3..ec89f904e4f4 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 @@ -136,6 +136,7 @@ public Object getObject() throws Exception { * or {@code null} if not known in advance. */ @Override + @Nullable public Class getObjectType() { if (!isPrepared()) { // Not fully initialized yet -> return null to indicate "not known yet". diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java index b86e633f744f..348b4674b7aa 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 @@ -224,6 +224,7 @@ public Object getObject() throws BeansException { } @Override + @Nullable public 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 130f13e310f6..0fba4f79c229 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ package org.springframework.beans.factory.config; +import java.util.Map; import java.util.Properties; import org.springframework.beans.BeansException; -import org.springframework.core.Constants; 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; import org.springframework.util.StringValueResolver; @@ -45,6 +46,7 @@ * * @author Juergen Hoeller * @author Chris Beams + * @author Sam Brannen * @since 02.10.2003 * @see #setSystemPropertiesModeName * @see PlaceholderConfigurerSupport @@ -72,7 +74,16 @@ public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport public static final int SYSTEM_PROPERTIES_MODE_OVERRIDE = 2; - private static final Constants constants = new Constants(PropertyPlaceholderConfigurer.class); + /** + * Map of constant names to constant values for the system properties mode + * constants defined in this class. + */ + private static final Map constants = Map.of( + "SYSTEM_PROPERTIES_MODE_NEVER", SYSTEM_PROPERTIES_MODE_NEVER, + "SYSTEM_PROPERTIES_MODE_FALLBACK", SYSTEM_PROPERTIES_MODE_FALLBACK, + "SYSTEM_PROPERTIES_MODE_OVERRIDE", SYSTEM_PROPERTIES_MODE_OVERRIDE + ); + private int systemPropertiesMode = SYSTEM_PROPERTIES_MODE_FALLBACK; @@ -87,7 +98,10 @@ public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport * @see #setSystemPropertiesMode */ public void setSystemPropertiesModeName(String constantName) throws IllegalArgumentException { - this.systemPropertiesMode = constants.asNumber(constantName).intValue(); + Assert.hasText(constantName, "'constantName' must not be null or blank"); + Integer mode = constants.get(constantName); + Assert.notNull(mode, "Only system properties mode constants allowed"); + this.systemPropertiesMode = mode; } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java index 8d0cf7dd397e..590280998d59 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 @@ -335,6 +335,7 @@ public Object getObject() { } @Override + @Nullable public Class getObjectType() { return this.serviceLocatorInterface; } 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 79b91cfe10b4..c4d9c5c8e540 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 @@ -16,6 +16,8 @@ package org.springframework.beans.factory.config; +import java.util.Comparator; + import org.springframework.beans.BeanMetadataElement; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -35,7 +37,7 @@ * @see BeanDefinition#getPropertyValues * @see org.springframework.beans.MutablePropertyValues#addPropertyValue */ -public class TypedStringValue implements BeanMetadataElement { +public class TypedStringValue implements BeanMetadataElement, Comparable { @Nullable private String value; @@ -213,6 +215,10 @@ public boolean isDynamic() { return this.dynamic; } + @Override + public int compareTo(@Nullable TypedStringValue o) { + return Comparator.comparing(TypedStringValue::getValue).compare(this, o); + } @Override public boolean equals(@Nullable Object other) { @@ -223,7 +229,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.value) * 29 + ObjectUtils.nullSafeHashCode(this.targetType); + return ObjectUtils.nullSafeHash(this.value, this.targetType); } @Override 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 cb6c2f16d587..ea482766d782 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 @@ -64,7 +64,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 1.18 or higher, as of Spring Framework 5.0.6. + *

Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. * * @author Dave Syer * @author Juergen Hoeller 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 3af6b2ab80f0..1b1fae321279 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 @@ -33,7 +33,10 @@ import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.composer.ComposerException; import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.inspector.TagInspector; +import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.reader.UnicodeReader; import org.yaml.snakeyaml.representer.Representer; @@ -47,7 +50,7 @@ /** * Base class for YAML factories. * - *

Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6. + *

Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. * * @author Dave Syer * @author Juergen Hoeller @@ -132,7 +135,7 @@ public void setResources(Resource... resources) { *

If no supported types are configured, only Java standard classes * (as defined in {@link org.yaml.snakeyaml.constructor.SafeConstructor}) * encountered in YAML documents will be supported. - * If an unsupported type is encountered, an {@link IllegalStateException} + * If an unsupported type is encountered, a {@link ComposerException} * will be thrown when the corresponding YAML node is processed. * @param supportedTypes the supported types, or an empty array to clear the * supported types @@ -173,19 +176,20 @@ protected void process(MatchCallback callback) { /** * Create the {@link Yaml} instance to use. *

The default implementation sets the "allowDuplicateKeys" flag to {@code false}, - * enabling built-in duplicate key handling in SnakeYAML 1.18+. - *

As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes - * supported types} have been configured, the default implementation creates - * a {@code Yaml} instance that filters out unsupported types encountered in - * YAML documents. If an unsupported type is encountered, an - * {@link IllegalStateException} will be thrown when the node is processed. + * enabling built-in duplicate key handling. + *

If custom {@linkplain #setSupportedTypes supported types} have been configured, + * the default implementation creates a {@code Yaml} instance that filters out + * unsupported types encountered in YAML documents. + * If an unsupported type is encountered, a {@link ComposerException} will be + * thrown when the node is processed. * @see LoaderOptions#setAllowDuplicateKeys(boolean) */ protected Yaml createYaml() { LoaderOptions loaderOptions = new LoaderOptions(); loaderOptions.setAllowDuplicateKeys(false); + loaderOptions.setTagInspector(new SupportedTagInspector()); DumperOptions dumperOptions = new DumperOptions(); - return new Yaml(new FilteringConstructor(loaderOptions), new Representer(dumperOptions), + return new Yaml(new Constructor(loaderOptions), new Representer(dumperOptions), dumperOptions, loaderOptions); } @@ -425,23 +429,11 @@ public enum ResolutionMethod { FIRST_FOUND } - - /** - * {@link Constructor} that supports filtering of unsupported types. - *

If an unsupported type is encountered in a YAML document, an - * {@link IllegalStateException} will be thrown from {@link #getClassForName}. - */ - private class FilteringConstructor extends Constructor { - - FilteringConstructor(LoaderOptions loaderOptions) { - super(loaderOptions); - } + private class SupportedTagInspector implements TagInspector { @Override - protected Class getClassForName(String name) throws ClassNotFoundException { - Assert.state(YamlProcessor.this.supportedTypes.contains(name), - () -> "Unsupported type encountered in YAML document: " + name); - return super.getClassForName(name); + public boolean isGlobalTagAllowed(Tag tag) { + return supportedTypes.contains(tag.getClassName()); } } 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 71088f17f09c..0c70f097d7c0 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,7 +74,7 @@ * servers[1]=foo.bar.com * * - *

Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6. + *

Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. * * @author Dave Syer * @author Stephane Nicoll 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 a01b8ed0367c..b098663f65ba 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,7 @@ 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; @@ -250,6 +251,7 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin @SuppressWarnings("serial") Closure beans = new Closure<>(this) { @Override + @Nullable public Object call(Object... args) { invokeBeanDefiningClosure((Closure) args[0]); return null; @@ -425,6 +427,7 @@ else if (args.length > 1 && args[args.length -1] instanceof Closure) { private boolean addDeferredProperty(String property, Object newValue) { if (newValue instanceof List || newValue instanceof Map) { + Assert.state(this.currentBeanDefinition != null, "No current bean definition set"); this.deferredProperties.put(this.currentBeanDefinition.getBeanName() + '.' + property, new DeferredProperty(this.currentBeanDefinition, property, newValue)); return true; @@ -476,7 +479,7 @@ private GroovyBeanDefinitionWrapper invokeBeanDefiningMethod(String beanName, Ob this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, beanClass); } } - else { + else { this.currentBeanDefinition = new GroovyBeanDefinitionWrapper( beanName, beanClass, resolveConstructorArguments(args, 1, args.length)); } @@ -489,7 +492,7 @@ else if (args[0] instanceof Map namedArgs) { // named constructor arguments if (args.length > 1 && args[1] instanceof Class clazz) { List constructorArgs = - resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length); + resolveConstructorArguments(args, 2, (hasClosureArgument ? args.length - 1 : args.length)); this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, clazz, constructorArgs); for (Map.Entry entity : namedArgs.entrySet()) { String propName = (String) entity.getKey(); @@ -525,7 +528,7 @@ else if (args[0] instanceof Closure) { } else { List constructorArgs = - resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length); + resolveConstructorArguments(args, 0, (hasClosureArgument ? args.length - 1 : args.length)); this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null, constructorArgs); } @@ -640,6 +643,7 @@ else if (value instanceof Closure callable) { this.currentBeanDefinition = current; } } + Assert.state(this.currentBeanDefinition != null, "No current bean definition set"); this.currentBeanDefinition.addProperty(name, value); } @@ -654,6 +658,7 @@ else if (value instanceof Closure callable) { * */ @Override + @Nullable public Object getProperty(String name) { Binding binding = getBinding(); if (binding != null && binding.hasVariable(name)) { @@ -727,9 +732,10 @@ private static class DeferredProperty { private final String name; + @Nullable public Object value; - public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, Object value) { + public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, @Nullable Object value) { this.beanDefinition = beanDefinition; this.name = name; this.value = value; @@ -762,6 +768,7 @@ public MetaClass getMetaClass() { } @Override + @Nullable public Object getProperty(String property) { if (property.equals("beanName")) { return getBeanName(); @@ -769,13 +776,10 @@ public Object getProperty(String property) { else if (property.equals("source")) { return getSource(); } - else if (this.beanDefinition != null) { + else { return new GroovyPropertyValue( property, this.beanDefinition.getBeanDefinition().getPropertyValues().get(property)); } - else { - return this.metaClass.getProperty(this, property); - } } @Override @@ -804,9 +808,10 @@ private class GroovyPropertyValue extends GroovyObjectSupport { private final String propertyName; + @Nullable private final Object propertyValue; - public GroovyPropertyValue(String propertyName, Object propertyValue) { + public GroovyPropertyValue(String propertyName, @Nullable Object propertyValue) { this.propertyName = propertyName; this.propertyValue = propertyValue; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java index e52922da0368..895646d9da0b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,7 +84,7 @@ class GroovyBeanDefinitionWrapper extends GroovyObjectSupport { this(beanName, clazz, null); } - GroovyBeanDefinitionWrapper(@Nullable String beanName, Class clazz, @Nullable Collection constructorArgs) { + GroovyBeanDefinitionWrapper(@Nullable String beanName, @Nullable Class clazz, @Nullable Collection constructorArgs) { this.beanName = beanName; this.clazz = clazz; this.constructorArgs = constructorArgs; @@ -130,11 +130,12 @@ void setBeanDefinitionHolder(BeanDefinitionHolder holder) { } BeanDefinitionHolder getBeanDefinitionHolder() { - return new BeanDefinitionHolder(getBeanDefinition(), getBeanName()); + Assert.state(this.beanName != null, "Bean name must be set"); + return new BeanDefinitionHolder(getBeanDefinition(), this.beanName); } - void setParent(Object obj) { - Assert.notNull(obj, "Parent bean cannot be set to a null runtime bean reference."); + void setParent(@Nullable Object obj) { + Assert.notNull(obj, "Parent bean cannot be set to a null runtime bean reference"); if (obj instanceof String name) { this.parentName = name; } @@ -148,7 +149,7 @@ else if (obj instanceof GroovyBeanDefinitionWrapper wrapper) { getBeanDefinition().setAbstract(false); } - GroovyBeanDefinitionWrapper addProperty(String propertyName, Object propertyValue) { + GroovyBeanDefinitionWrapper addProperty(String propertyName, @Nullable Object propertyValue) { if (propertyValue instanceof GroovyBeanDefinitionWrapper wrapper) { propertyValue = wrapper.getBeanDefinition(); } @@ -158,6 +159,7 @@ GroovyBeanDefinitionWrapper addProperty(String propertyName, Object propertyValu @Override + @Nullable public Object getProperty(String property) { Assert.state(this.definitionWrapper != null, "BeanDefinition wrapper not initialized"); if (this.definitionWrapper.isReadableProperty(property)) { @@ -170,7 +172,7 @@ else if (dynamicProperties.contains(property)) { } @Override - public void setProperty(String property, Object newValue) { + public void setProperty(String property, @Nullable Object newValue) { if (PARENT.equals(property)) { setParent(newValue); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java index 0277bc0a0f9c..afbb13957125 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,9 +98,7 @@ public String toString() { for (ParseState.Entry entry : this.state) { if (i > 0) { sb.append('\n'); - for (int j = 0; j < i; j++) { - sb.append('\t'); - } + sb.append("\t".repeat(i)); sb.append("-> "); } sb.append(entry); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 7de716b44c95..ae9656818cc0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; +import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessorUtils; import org.springframework.beans.PropertyValue; @@ -92,10 +93,10 @@ * Supports autowiring constructors, properties by name, and properties by type. * *

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

Note that this class does not assume or implement bean definition * registry capabilities. See {@link DefaultListableBeanFactory} for an implementation @@ -357,6 +358,7 @@ public Object configureBean(Object existingBean, String beanName) throws BeansEx // Specialized methods for fine-grained control over the bean lifecycle //------------------------------------------------------------------------- + @Deprecated @Override public Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException { // Use non-singleton bean definition, to avoid registering bean as dependent bean. @@ -410,6 +412,7 @@ public Object initializeBean(Object existingBean, String beanName) { return initializeBean(beanName, existingBean, null); } + @Deprecated(since = "6.1") @Override public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException { @@ -425,6 +428,7 @@ public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, S return result; } + @Deprecated(since = "6.1") @Override public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException { @@ -493,15 +497,13 @@ protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable O if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); - } - - // Prepare method overrides. - try { - mbdToUse.prepareMethodOverrides(); - } - catch (BeanDefinitionValidationException ex) { - throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), - beanName, "Validation of method overrides failed", ex); + try { + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } } try { @@ -652,7 +654,7 @@ protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Clas // Apply SmartInstantiationAwareBeanPostProcessors to predict the // eventual type after a before-instantiation shortcut. if (targetType != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { - boolean matchingOnlyFactoryBean = typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class; + boolean matchingOnlyFactoryBean = (typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class); for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { Class predicted = bp.predictBeanType(targetType, beanName); if (predicted != null && @@ -811,30 +813,48 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m // Common return type found: all factory methods return same type. For a non-parameterized // unique candidate, cache the full type declaration context of the target factory method. - cachedReturnType = (uniqueCandidate != null ? - ResolvableType.forMethodReturnType(uniqueCandidate) : ResolvableType.forClass(commonType)); - mbd.factoryMethodReturnType = cachedReturnType; - return cachedReturnType.resolve(); + try { + cachedReturnType = (uniqueCandidate != null ? + ResolvableType.forMethodReturnType(uniqueCandidate) : ResolvableType.forClass(commonType)); + mbd.factoryMethodReturnType = cachedReturnType; + return cachedReturnType.resolve(); + } + catch (LinkageError err) { + // E.g. a NoClassDefFoundError for a generic method return type + if (logger.isDebugEnabled()) { + logger.debug("Failed to resolve type for factory method of bean '" + beanName + "': " + + (uniqueCandidate != null ? uniqueCandidate : commonType), err); + } + return null; + } } /** * This implementation attempts to query the FactoryBean's generic parameter metadata * if present to determine the object type. If not present, i.e. the FactoryBean is - * declared as a raw type, checks the FactoryBean's {@code getObjectType} method + * declared as a raw type, it checks the FactoryBean's {@code getObjectType} method * on a plain instance of the FactoryBean, without bean properties applied yet. - * If this doesn't return a type yet, and {@code allowInit} is {@code true} a - * full creation of the FactoryBean is used as fallback (through delegation to the - * superclass's implementation). + * If this doesn't return a type yet and {@code allowInit} is {@code true}, full + * creation of the FactoryBean is attempted as fallback (through delegation to the + * superclass implementation). *

The shortcut check for a FactoryBean is only applied in case of a singleton * FactoryBean. If the FactoryBean instance itself is not kept as singleton, * it will be fully created to check the type of its exposed object. */ @Override protected ResolvableType getTypeForFactoryBean(String beanName, RootBeanDefinition mbd, boolean allowInit) { + ResolvableType result; + // Check if the bean definition itself has defined the type with an attribute - ResolvableType result = getTypeForFactoryBeanFromAttributes(mbd); - if (result != ResolvableType.NONE) { - return result; + try { + result = getTypeForFactoryBeanFromAttributes(mbd); + if (result != ResolvableType.NONE) { + return result; + } + } + catch (IllegalArgumentException ex) { + throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName, + String.valueOf(ex.getMessage())); } // For instance supplied beans, try the target type and bean class immediately @@ -1091,6 +1111,7 @@ protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, C * @param mbd the bean definition for the bean * @return the shortcut-determined bean instance, or {@code null} if none */ + @SuppressWarnings("deprecation") @Nullable protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { Object bean = null; @@ -1153,9 +1174,11 @@ protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); } - Supplier instanceSupplier = mbd.getInstanceSupplier(); - if (instanceSupplier != null) { - return obtainFromSupplier(instanceSupplier, beanName, mbd); + if (args == null) { + Supplier instanceSupplier = mbd.getInstanceSupplier(); + if (instanceSupplier != null) { + return obtainFromSupplier(instanceSupplier, beanName, mbd); + } } if (mbd.getFactoryMethodName() != null) { @@ -1681,8 +1704,7 @@ protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrap } Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); Object convertedValue = resolvedValue; - boolean convertible = bw.isWritableProperty(propertyName) && - !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); + boolean convertible = isConvertibleProperty(propertyName, bw); if (convertible) { convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); } @@ -1719,6 +1741,19 @@ else if (convertible && originalValue instanceof TypedStringValue typedStringVal } } + /** + * Determine whether the factory should cache a converted value for the given property. + */ + private boolean isConvertibleProperty(String propertyName, BeanWrapper bw) { + try { + return !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName) && + BeanUtils.hasUniqueWriteMethod(bw.getPropertyDescriptor(propertyName)); + } + catch (InvalidPropertyException ex) { + return false; + } + } + /** * Convert the given value for the specified target property. */ @@ -1754,6 +1789,7 @@ private Object convertForProperty( * @see #invokeInitMethods * @see #applyBeanPostProcessorsAfterInitialization */ + @SuppressWarnings("deprecation") protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) { invokeAwareMethods(beanName, bean); @@ -1885,6 +1921,7 @@ protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefi * object obtained from FactoryBeans (for example, to auto-proxy them). * @see #applyBeanPostProcessorsAfterInitialization */ + @SuppressWarnings("deprecation") @Override protected Object postProcessObjectFromFactoryBean(Object object, String beanName) { return applyBeanPostProcessorsAfterInitialization(object, beanName); @@ -1941,6 +1978,10 @@ public CreateFromClassBeanDefinition(CreateFromClassBeanDefinition original) { @Override @Nullable public Constructor[] getPreferredConstructors() { + Constructor[] fromAttribute = super.getPreferredConstructors(); + if (fromAttribute != null) { + return fromAttribute; + } return ConstructorResolver.determinePreferredConstructors(getBeanClass()); } 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 a81613d3bd6b..ebdadd211b6b 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 @@ -51,6 +51,7 @@ * @author Juergen Hoeller * @author Rob Harrop * @author Mark Fisher + * @author Sebastien Deleuze * @see GenericBeanDefinition * @see RootBeanDefinition * @see ChildBeanDefinition @@ -125,6 +126,32 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess */ public static final int DEPENDENCY_CHECK_ALL = 3; + /** + * The name of an attribute that can be + * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a + * {@link org.springframework.beans.factory.config.BeanDefinition} so that + * bean definitions can indicate one or more preferred constructors. This is + * analogous to {@code @Autowired} annotated constructors on the bean class. + *

The attribute value may be a single {@link java.lang.reflect.Constructor} + * reference or an array thereof. + * @since 6.1 + * @see org.springframework.beans.factory.annotation.Autowired + * @see org.springframework.beans.factory.support.RootBeanDefinition#getPreferredConstructors() + */ + public static final String PREFERRED_CONSTRUCTORS_ATTRIBUTE = "preferredConstructors"; + + /** + * The name of an attribute that can be + * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a + * {@link org.springframework.beans.factory.config.BeanDefinition} so that + * bean definitions can indicate the sort order for the targeted bean. + * This is analogous to the {@code @Order} annotation. + * @since 6.1.2 + * @see org.springframework.core.annotation.Order + * @see org.springframework.core.Ordered + */ + public static final String ORDER_ATTRIBUTE = "order"; + /** * Constant that indicates the container should attempt to infer the * {@link #setDestroyMethodName destroy method name} for a bean as opposed to @@ -554,7 +581,7 @@ public void setLazyInit(boolean lazyInit) { */ @Override public boolean isLazyInit() { - return (this.lazyInit != null && this.lazyInit.booleanValue()); + return (this.lazyInit != null && this.lazyInit); } /** 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 57b9b8dffad8..824d86e29e22 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -161,6 +161,9 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp /** Map from scope identifier String to corresponding Scope. */ private final Map scopes = new LinkedHashMap<>(8); + /** Application startup metrics. **/ + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + /** Map from bean name to merged RootBeanDefinition. */ private final Map mergedBeanDefinitions = new ConcurrentHashMap<>(256); @@ -171,8 +174,6 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp private final ThreadLocal prototypesCurrentlyInCreation = new NamedThreadLocal<>("Prototype beans currently in creation"); - /** Application startup metrics. **/ - private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; /** * Create a new AbstractBeanFactory. @@ -315,6 +316,17 @@ else if (requiredType != null) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", ex); } + catch (BeanCreationException ex) { + if (requiredType != null) { + // Wrap exception with current bean metadata but only if specifically + // requested (indicated by required type), not for depends-on cascades. + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Failed to initialize dependency '" + ex.getBeanName() + "' of " + + requiredType.getSimpleName() + " bean '" + beanName + "': " + + ex.getMessage(), ex); + } + throw ex; + } } } @@ -382,6 +394,9 @@ else if (mbd.isPrototype()) { } finally { beanCreation.end(); + if (!isCacheBeanMetadata()) { + clearMergedBeanDefinition(beanName); + } } } @@ -521,42 +536,73 @@ protected boolean isTypeMatch(String name, ResolvableType typeToMatch, boolean a // Check manually registered singletons. Object beanInstance = getSingleton(beanName, false); if (beanInstance != null && beanInstance.getClass() != NullBean.class) { + + // Determine target for FactoryBean match if necessary. if (beanInstance instanceof FactoryBean factoryBean) { if (!isFactoryDereference) { Class type = getTypeForFactoryBean(factoryBean); - return (type != null && typeToMatch.isAssignableFrom(type)); - } - else { - return typeToMatch.isInstance(beanInstance); - } - } - else if (!isFactoryDereference) { - if (typeToMatch.isInstance(beanInstance)) { - // Direct match for exposed instance? - return true; - } - else if (typeToMatch.hasGenerics() && containsBeanDefinition(beanName)) { - // Generics potentially only match on the target class, not on the proxy... - RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); - Class targetType = mbd.getTargetType(); - if (targetType != null && targetType != ClassUtils.getUserClass(beanInstance)) { - // Check raw class match as well, making sure it's exposed on the proxy. - Class classToMatch = typeToMatch.resolve(); - if (classToMatch != null && !classToMatch.isInstance(beanInstance)) { + if (type == null) { + return false; + } + if (typeToMatch.isAssignableFrom(type)) { + return true; + } + else if (typeToMatch.hasGenerics() && containsBeanDefinition(beanName)) { + RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + ResolvableType targetType = mbd.targetType; + if (targetType == null) { + targetType = mbd.factoryMethodReturnType; + } + if (targetType == null) { return false; } - if (typeToMatch.isAssignableFrom(targetType)) { - return true; + Class targetClass = targetType.resolve(); + if (targetClass != null && FactoryBean.class.isAssignableFrom(targetClass)) { + Class classToMatch = typeToMatch.resolve(); + if (classToMatch != null && !FactoryBean.class.isAssignableFrom(classToMatch) && + !classToMatch.isAssignableFrom(targetType.toClass())) { + return typeToMatch.isAssignableFrom(targetType.getGeneric()); + } + } + else { + return typeToMatch.isAssignableFrom(targetType); } } - ResolvableType resolvableType = mbd.targetType; - if (resolvableType == null) { - resolvableType = mbd.factoryMethodReturnType; + return false; + } + } + else if (isFactoryDereference) { + return false; + } + + // Actual matching against bean instance... + if (typeToMatch.isInstance(beanInstance)) { + // Direct match for exposed instance? + return true; + } + else if (typeToMatch.hasGenerics() && containsBeanDefinition(beanName)) { + // Generics potentially only match on the target class, not on the proxy... + RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + Class targetType = mbd.getTargetType(); + if (targetType != null && targetType != ClassUtils.getUserClass(beanInstance)) { + // Check raw class match as well, making sure it's exposed on the proxy. + Class classToMatch = typeToMatch.resolve(); + if (classToMatch != null && !classToMatch.isInstance(beanInstance)) { + return false; + } + if (typeToMatch.isAssignableFrom(targetType)) { + return true; } - return (resolvableType != null && typeToMatch.isAssignableFrom(resolvableType)); } + ResolvableType resolvableType = mbd.targetType; + if (resolvableType == null) { + resolvableType = mbd.factoryMethodReturnType; + } + return (resolvableType != null && typeToMatch.isAssignableFrom(resolvableType)); + } + else { + return false; } - return false; } else if (containsSingleton(beanName) && !containsBeanDefinition(beanName)) { // null instance registered @@ -731,7 +777,7 @@ public String[] getAliases(String name) { aliases.add(fullBeanName); } String[] retrievedAliases = super.getAliases(beanName); - String prefix = factoryPrefix ? FACTORY_BEAN_PREFIX : ""; + String prefix = (factoryPrefix ? FACTORY_BEAN_PREFIX : ""); for (String retrievedAlias : retrievedAliases) { String alias = prefix + retrievedAlias; if (!alias.equals(name)) { @@ -1052,7 +1098,7 @@ public Scope getRegisteredScope(String scopeName) { @Override public void setApplicationStartup(ApplicationStartup applicationStartup) { - Assert.notNull(applicationStartup, "applicationStartup must not be null"); + Assert.notNull(applicationStartup, "ApplicationStartup must not be null"); this.applicationStartup = applicationStartup; } @@ -1407,7 +1453,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()) { + if (containingBd == null && (isCacheBeanMetadata() || isBeanEligibleForMetadataCaching(beanName))) { this.mergedBeanDefinitions.put(beanName, mbd); } } @@ -1431,6 +1477,9 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous, mbd.factoryMethodReturnType = previous.factoryMethodReturnType; mbd.factoryMethodToIntrospect = previous.factoryMethodToIntrospect; } + if (previous.hasMethodOverrides()) { + mbd.setMethodOverrides(new MethodOverrides(previous.getMethodOverrides())); + } } } @@ -1497,7 +1546,11 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla if (mbd.hasBeanClass()) { return mbd.getBeanClass(); } - return doResolveBeanClass(mbd, typesToMatch); + Class beanClass = doResolveBeanClass(mbd, typesToMatch); + if (mbd.hasBeanClass()) { + mbd.prepareMethodOverrides(); + } + return beanClass; } catch (ClassNotFoundException ex) { throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex); @@ -1505,6 +1558,10 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla catch (LinkageError err) { throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), err); } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbd.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } } @Nullable @@ -1640,7 +1697,7 @@ protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) { * already. The implementation is allowed to instantiate the target factory bean if * {@code allowInit} is {@code true} and the type cannot be determined another way; * otherwise it is restricted to introspecting signatures and related metadata. - *

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

If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} is set on the bean definition * and {@code allowInit} is {@code true}, the default implementation will create * the FactoryBean via {@code getBean} to call its {@code getObjectType} method. * Subclasses are encouraged to optimize this, typically by inspecting the generic @@ -1659,9 +1716,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()) { 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 b085f760c024..6baa1fd13880 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 @@ -53,7 +53,7 @@ abstract class AutowireUtils { public static final Comparator EXECUTABLE_COMPARATOR = (e1, e2) -> { int result = Boolean.compare(Modifier.isPublic(e2.getModifiers()), Modifier.isPublic(e1.getModifiers())); - return result != 0 ? result : Integer.compare(e2.getParameterCount(), e1.getParameterCount()); + return (result != 0 ? result : Integer.compare(e2.getParameterCount(), e1.getParameterCount())); }; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java index 5da53bfe3b67..2aecad8b437a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -59,7 +59,7 @@ public void setLazyInit(boolean lazyInit) { * @return whether to apply lazy-init semantics ({@code false} by default) */ public boolean isLazyInit() { - return (this.lazyInit != null && this.lazyInit.booleanValue()); + return (this.lazyInit != null && this.lazyInit); } /** 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 a1f47e536030..f5368ea9e486 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-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,6 +97,19 @@ void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) */ int getBeanDefinitionCount(); + /** + * Determine whether the bean definition for the given name is overridable, + * i.e. whether {@link #registerBeanDefinition} would successfully return + * against an existing definition of the same name. + *

The default implementation returns {@code true}. + * @param beanName the name to check + * @return whether the definition for the given bean name is overridable + * @since 6.1 + */ + default boolean isBeanDefinitionOverridable(String beanName) { + return true; + } + /** * Determine whether the given bean name is already in use within this registry, * i.e. whether there is a local bean or alias registered under this name. 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 b94f1ab5ab66..13763ee337ac 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-2010 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; /** * Extension to the standard {@link BeanFactoryPostProcessor} SPI, allowing for @@ -42,4 +43,14 @@ public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProc */ void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException; + /** + * Empty implementation of {@link BeanFactoryPostProcessor#postProcessBeanFactory} + * since custom {@code BeanDefinitionRegistryPostProcessor} implementations will + * typically only provide a {@link #postProcessBeanDefinitionRegistry} method. + * @since 6.1 + */ + @Override + default void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java index b3da7fb62b44..551e0050a9ff 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 @@ -241,6 +241,7 @@ public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFa } @Override + @Nullable public 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); 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 6af615a32049..45b15772f014 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,6 +61,7 @@ import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.core.CollectionFactory; import org.springframework.core.MethodParameter; import org.springframework.core.NamedThreadLocal; @@ -120,11 +121,7 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { /** * "autowire constructor" (with constructor arguments by type) behavior. - * Also applied if explicit constructor argument values are specified, - * matching all remaining arguments with beans from the bean factory. - *

This corresponds to constructor injection: In this mode, a Spring - * bean factory is able to host components that expect constructor-based - * dependency resolution. + * Also applied if explicit constructor argument values are specified. * @param beanName the name of the bean * @param mbd the merged bean definition for the bean * @param chosenCtors chosen candidate constructors (or {@code null} if none) @@ -613,13 +610,10 @@ else if (resolvedValues != null) { String argDesc = StringUtils.collectionToCommaDelimitedString(argTypes); throw new BeanCreationException(mbd.getResourceDescription(), beanName, "No matching factory method found on class [" + factoryClass.getName() + "]: " + - (mbd.getFactoryBeanName() != null ? - "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + + (mbd.getFactoryBeanName() != null ? "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + "factory method '" + mbd.getFactoryMethodName() + "(" + argDesc + ")'. " + - "Check that a method with the specified name " + - (minNrOfArgs > 0 ? "and arguments " : "") + - "exists and that it is " + - (isStatic ? "static" : "non-static") + "."); + "Check that a method with the specified name " + (minNrOfArgs > 0 ? "and arguments " : "") + + "exists and that it is " + (isStatic ? "static" : "non-static") + "."); } else if (void.class == factoryMethodToUse.getReturnType()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, @@ -917,7 +911,7 @@ Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramT // 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. if (paramType.isArray()) { - return Array.newInstance(paramType.getComponentType(), 0); + return Array.newInstance(paramType.componentType(), 0); } else if (CollectionFactory.isApproximableCollectionType(paramType)) { return CollectionFactory.createCollection(paramType, 0); @@ -999,6 +993,9 @@ private List determineParameterValueTypes(RootBeanDefinition mbd for (ValueHolder valueHolder : mbd.getConstructorArgumentValues().getIndexedArgumentValues().values()) { parameterTypes.add(determineParameterValueType(mbd, valueHolder)); } + for (ValueHolder valueHolder : mbd.getConstructorArgumentValues().getGenericArgumentValues()) { + parameterTypes.add(determineParameterValueType(mbd, valueHolder)); + } return parameterTypes; } @@ -1023,6 +1020,12 @@ private ResolvableType determineParameterValueType(RootBeanDefinition mbd, Value return (FactoryBean.class.isAssignableFrom(type.toClass()) ? type.as(FactoryBean.class).getGeneric(0) : type); } + if (value instanceof TypedStringValue typedValue) { + if (typedValue.hasTargetType()) { + return ResolvableType.forClass(typedValue.getTargetType()); + } + return ResolvableType.forClass(String.class); + } if (value instanceof Class clazz) { return ResolvableType.forClassWithGenerics(Class.class, clazz); } @@ -1190,7 +1193,7 @@ private boolean isMatch(ResolvableType parameterType, ResolvableType valueType, } private Predicate isAssignable(ResolvableType valueType) { - return parameterType -> parameterType.isAssignableFrom(valueType); + return parameterType -> (valueType == ResolvableType.NONE || parameterType.isAssignableFrom(valueType)); } private ResolvableType extractElementType(ResolvableType parameterType) { @@ -1431,6 +1434,7 @@ public boolean hasShortcut() { } @Override + @Nullable public Object resolveShortcut(BeanFactory beanFactory) { String shortcut = this.shortcut; return (shortcut != null ? beanFactory.getBean(shortcut, getDependencyType()) : null); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index d8c54764cd29..d119caefd3be 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,6 +70,7 @@ import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.NamedBeanHolder; import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -110,6 +111,7 @@ * @author Chris Beams * @author Phillip Webb * @author Stephane Nicoll + * @author Sebastien Deleuze * @since 16 April 2001 * @see #registerBeanDefinition * @see #addBeanPostProcessor @@ -121,16 +123,16 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { @Nullable - private static Class javaxInjectProviderClass; + private static 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; } } @@ -1011,7 +1013,7 @@ public void registerBeanDefinition(String beanName, BeanDefinition beanDefinitio BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName); if (existingDefinition != null) { - if (!isAllowBeanDefinitionOverriding()) { + if (!isBeanDefinitionOverridable(beanName)) { throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition); } else if (existingDefinition.getRole() < beanDefinition.getRole()) { @@ -1040,8 +1042,8 @@ else if (!beanDefinition.equals(existingDefinition)) { } else { if (isAlias(beanName)) { - if (!isAllowBeanDefinitionOverriding()) { - String aliasedName = canonicalName(beanName); + String aliasedName = canonicalName(beanName); + if (!isBeanDefinitionOverridable(aliasedName)) { if (containsBeanDefinition(aliasedName)) { // alias for existing bean definition throw new BeanDefinitionOverrideException( beanName, beanDefinition, getBeanDefinition(aliasedName)); @@ -1150,8 +1152,19 @@ protected void resetBeanDefinition(String beanName) { } } + /** + * This implementation returns {@code true} if bean definition overriding + * is generally allowed. + * @see #setAllowBeanDefinitionOverriding + */ + @Override + public boolean isBeanDefinitionOverridable(String beanName) { + return isAllowBeanDefinitionOverriding(); + } + /** * Only allows alias overriding if bean definition overriding is allowed. + * @see #setAllowBeanDefinitionOverriding */ @Override protected boolean allowAliasOverriding() { @@ -1164,7 +1177,7 @@ protected boolean allowAliasOverriding() { @Override protected void checkForAliasCircle(String name, String alias) { super.checkForAliasCircle(name, alias); - if (!isAllowBeanDefinitionOverriding() && containsBeanDefinition(alias)) { + if (!isBeanDefinitionOverridable(alias) && containsBeanDefinition(alias)) { throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + name + "': Alias would override bean definition '" + alias + "'"); } @@ -1327,17 +1340,17 @@ else if (ObjectFactory.class == descriptor.getDependencyType() || ObjectProvider.class == descriptor.getDependencyType()) { return new DependencyObjectProvider(descriptor, requestingBeanName); } - else if (javaxInjectProviderClass == descriptor.getDependencyType()) { + else if (jakartaInjectProviderClass == descriptor.getDependencyType()) { return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName); } - else { + else if (descriptor.supportsLazyResolution()) { Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary( descriptor, requestingBeanName); - if (result == null) { - result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); + if (result != null) { + return result; } - return result; } + return doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } @Nullable @@ -1346,12 +1359,15 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor); try { + // Step 1: pre-resolved shortcut for single bean match, e.g. from @Autowired Object shortcut = descriptor.resolveShortcut(this); if (shortcut != null) { return shortcut; } Class type = descriptor.getDependencyType(); + + // Step 2: pre-defined value or expression, e.g. from @Value Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); if (value != null) { if (value instanceof String strValue) { @@ -1372,13 +1388,20 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } } + // Step 3a: multiple beans as stream / array / standard collection / plain map Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter); if (multipleBeans != null) { return multipleBeans; } - + // Step 3b: direct bean matches, possibly direct beans of type Collection / Map Map matchingBeans = findAutowireCandidates(beanName, type, descriptor); if (matchingBeans.isEmpty()) { + // Step 3c (fallback): custom Collection / Map declarations for collecting multiple beans + multipleBeans = resolveMultipleBeansFallback(descriptor, beanName, autowiredBeanNames, typeConverter); + if (multipleBeans != null) { + return multipleBeans; + } + // Raise exception if nothing found for required injection point if (isRequired(descriptor)) { raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); } @@ -1388,10 +1411,12 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str String autowiredBeanName; Object instanceCandidate; + // Step 4: determine single candidate if (matchingBeans.size() > 1) { autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); if (autowiredBeanName == null) { - if (isRequired(descriptor) || !indicatesMultipleBeans(type)) { + if (isRequired(descriptor) || !indicatesArrayCollectionOrMap(type)) { + // Raise exception if no clear match found for required injection point return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans); } else { @@ -1410,6 +1435,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str instanceCandidate = entry.getValue(); } + // Step 5: validate single result if (autowiredBeanNames != null) { autowiredBeanNames.add(autowiredBeanName); } @@ -1419,6 +1445,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str Object result = instanceCandidate; if (result instanceof NullBean) { if (isRequired(descriptor)) { + // Raise exception if null encountered for required injection point raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); } result = null; @@ -1453,7 +1480,7 @@ private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable S return stream; } else if (type.isArray()) { - Class componentType = type.getComponentType(); + Class componentType = type.componentType(); ResolvableType resolvableType = descriptor.getResolvableType(); Class resolvedArrayType = resolvableType.resolve(type); if (resolvedArrayType != type) { @@ -1480,63 +1507,92 @@ else if (type.isArray()) { } return result; } - else if (Collection.class.isAssignableFrom(type) && type.isInterface()) { - Class elementType = descriptor.getResolvableType().asCollection().resolveGeneric(); - if (elementType == null) { - return null; - } - Map matchingBeans = findAutowireCandidates(beanName, elementType, - new MultiElementDescriptor(descriptor)); - if (matchingBeans.isEmpty()) { - return null; - } - if (autowiredBeanNames != null) { - autowiredBeanNames.addAll(matchingBeans.keySet()); - } - TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); - Object result = converter.convertIfNecessary(matchingBeans.values(), type); - if (result instanceof List list && list.size() > 1) { - Comparator comparator = adaptDependencyComparator(matchingBeans); - if (comparator != null) { - list.sort(comparator); - } - } - return result; + else if (Collection.class == type || Set.class == type || List.class == type) { + return resolveMultipleBeanCollection(descriptor, beanName, autowiredBeanNames, typeConverter); } else if (Map.class == type) { - ResolvableType mapType = descriptor.getResolvableType().asMap(); - Class keyType = mapType.resolveGeneric(0); - if (String.class != keyType) { - return null; - } - Class valueType = mapType.resolveGeneric(1); - if (valueType == null) { - return null; - } - Map matchingBeans = findAutowireCandidates(beanName, valueType, - new MultiElementDescriptor(descriptor)); - if (matchingBeans.isEmpty()) { - return null; - } - if (autowiredBeanNames != null) { - autowiredBeanNames.addAll(matchingBeans.keySet()); - } - return matchingBeans; + return resolveMultipleBeanMap(descriptor, beanName, autowiredBeanNames, typeConverter); } - else { + return null; + } + + + @Nullable + private Object resolveMultipleBeansFallback(DependencyDescriptor descriptor, @Nullable String beanName, + @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { + + Class type = descriptor.getDependencyType(); + + if (Collection.class.isAssignableFrom(type) && type.isInterface()) { + return resolveMultipleBeanCollection(descriptor, beanName, autowiredBeanNames, typeConverter); + } + else if (Map.class.isAssignableFrom(type) && type.isInterface()) { + return resolveMultipleBeanMap(descriptor, beanName, autowiredBeanNames, typeConverter); + } + return null; + } + + @Nullable + private Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @Nullable String beanName, + @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { + + Class elementType = descriptor.getResolvableType().asCollection().resolveGeneric(); + if (elementType == null) { + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, elementType, + new MultiElementDescriptor(descriptor)); + if (matchingBeans.isEmpty()) { return null; } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + Object result = converter.convertIfNecessary(matchingBeans.values(), descriptor.getDependencyType()); + if (result instanceof List list && list.size() > 1) { + Comparator comparator = adaptDependencyComparator(matchingBeans); + if (comparator != null) { + list.sort(comparator); + } + } + return result; } - private boolean isRequired(DependencyDescriptor descriptor) { - return getAutowireCandidateResolver().isRequired(descriptor); + @Nullable + private Object resolveMultipleBeanMap(DependencyDescriptor descriptor, @Nullable String beanName, + @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { + + ResolvableType mapType = descriptor.getResolvableType().asMap(); + Class keyType = mapType.resolveGeneric(0); + if (String.class != keyType) { + return null; + } + Class valueType = mapType.resolveGeneric(1); + if (valueType == null) { + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, valueType, + new MultiElementDescriptor(descriptor)); + if (matchingBeans.isEmpty()) { + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return converter.convertIfNecessary(matchingBeans, descriptor.getDependencyType()); } - private boolean indicatesMultipleBeans(Class type) { + private boolean indicatesArrayCollectionOrMap(Class type) { return (type.isArray() || (type.isInterface() && (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)))); } + private boolean isRequired(DependencyDescriptor descriptor) { + return getAutowireCandidateResolver().isRequired(descriptor); + } + @Nullable private Comparator adaptDependencyComparator(Map matchingBeans) { Comparator comparator = getDependencyComparator(); @@ -1598,7 +1654,7 @@ protected Map findAutowireCandidates( } } if (result.isEmpty()) { - boolean multiple = indicatesMultipleBeans(requiredType); + boolean multiple = indicatesArrayCollectionOrMap(requiredType); // Consider fallback matches if the first pass failed to find anything... DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch(); for (String candidate : candidateNames) { @@ -1757,7 +1813,7 @@ else if (candidatePriority < highestPriority) { * Return whether the bean definition for the given bean name has been * marked as a primary bean. * @param beanName the name of the bean - * @param beanInstance the corresponding bean instance (can be null) + * @param beanInstance the corresponding bean instance (can be {@code null}) * @return whether the given bean qualifies as primary */ protected boolean isPrimary(String beanName, Object beanInstance) { @@ -2176,7 +2232,9 @@ public Object get() throws BeansException { * that is aware of the bean metadata of the instances to sort. *

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

As of 6.1.2, this class takes the {@link AbstractBeanDefinition#ORDER_ATTRIBUTE} + * attribute into account. */ private class FactoryAwareOrderSourceProvider implements OrderComparator.OrderSourceProvider { @@ -2195,7 +2253,17 @@ public Object getOrderSource(Object obj) { } try { RootBeanDefinition beanDefinition = (RootBeanDefinition) getMergedBeanDefinition(beanName); - List sources = new ArrayList<>(2); + List sources = new ArrayList<>(3); + Object orderAttribute = beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); + if (orderAttribute != null) { + if (orderAttribute instanceof Integer order) { + sources.add((Ordered) () -> order); + } + else { + throw new IllegalStateException("Invalid value type for attribute '" + + AbstractBeanDefinition.ORDER_ATTRIBUTE + "': " + orderAttribute.getClass().getName()); + } + } Method factoryMethod = beanDefinition.getResolvedFactoryMethod(); if (factoryMethod != null) { sources.add(factoryMethod); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 9b189b34312d..81e442404973 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -567,16 +567,16 @@ public void destroySingleton(String beanName) { */ protected void destroyBean(String beanName, @Nullable DisposableBean bean) { // Trigger destruction of dependent beans first... - Set dependencies; + Set dependentBeanNames; synchronized (this.dependentBeanMap) { // Within full synchronization in order to guarantee a disconnected Set - dependencies = this.dependentBeanMap.remove(beanName); + dependentBeanNames = this.dependentBeanMap.remove(beanName); } - if (dependencies != null) { + if (dependentBeanNames != null) { if (logger.isTraceEnabled()) { - logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependencies); + logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependentBeanNames); } - for (String dependentBeanName : dependencies) { + for (String dependentBeanName : dependentBeanNames) { destroySingleton(dependentBeanName); } } 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 bd6e62fb463d..14198c4b2f1f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,20 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.DisposableBean; 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; @@ -68,6 +75,10 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class); + private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + "org.reactivestreams.Publisher", DisposableBeanAdapter.class.getClassLoader()); + + private final Object bean; private final String beanName; @@ -261,7 +272,7 @@ private Method determineDestroyMethod(String destroyMethodName) { if (destroyMethod != null) { return destroyMethod; } - for (Class beanInterface : beanClass.getInterfaces()) { + for (Class beanInterface : ClassUtils.getAllInterfacesForClass(beanClass)) { destroyMethod = findDestroyMethod(beanInterface, methodName); if (destroyMethod != null) { return destroyMethod; @@ -289,32 +300,40 @@ private Method findDestroyMethod(Class clazz, String name) { * assuming a "force" parameter), else logging an error. */ private void invokeCustomDestroyMethod(Method destroyMethod) { + if (logger.isTraceEnabled()) { + logger.trace("Invoking custom destroy method '" + destroyMethod.getName() + + "' on bean with name '" + this.beanName + "': " + destroyMethod); + } + int paramCount = destroyMethod.getParameterCount(); Object[] args = new Object[paramCount]; if (paramCount == 1) { args[0] = Boolean.TRUE; } - if (logger.isTraceEnabled()) { - logger.trace("Invoking custom destroy method '" + destroyMethod.getName() + - "' on bean with name '" + this.beanName + "'"); - } + try { ReflectionUtils.makeAccessible(destroyMethod); - destroyMethod.invoke(this.bean, args); - } - catch (InvocationTargetException ex) { - if (logger.isWarnEnabled()) { - String msg = "Custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + - this.beanName + "' threw an exception"; + Object returnValue = destroyMethod.invoke(this.bean, args); + + if (returnValue == null) { + // Regular case: a void method + logDestroyMethodCompletion(destroyMethod, false); + } + else if (returnValue instanceof Future future) { + // An async task: await its completion. + future.get(); + logDestroyMethodCompletion(destroyMethod, true); + } + else if (!reactiveStreamsPresent || !new ReactiveDestroyMethodHandler().await(destroyMethod, returnValue)) { if (logger.isDebugEnabled()) { - // Log at warn level like below but add the exception stacktrace only with debug level - logger.warn(msg, ex.getTargetException()); - } - else { - logger.warn(msg + ": " + ex.getTargetException()); + logger.debug("Unknown return value type from custom destroy method '" + destroyMethod.getName() + + "' on bean with name '" + this.beanName + "': " + returnValue.getClass()); } } } + catch (InvocationTargetException | ExecutionException ex) { + logDestroyMethodException(destroyMethod, ex.getCause()); + } catch (Throwable ex) { if (logger.isWarnEnabled()) { logger.warn("Failed to invoke custom destroy method '" + destroyMethod.getName() + @@ -323,6 +342,27 @@ private void invokeCustomDestroyMethod(Method destroyMethod) { } } + void logDestroyMethodException(Method destroyMethod, @Nullable Throwable ex) { + if (logger.isWarnEnabled()) { + String msg = "Custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + + this.beanName + "' propagated an exception"; + if (logger.isDebugEnabled()) { + // Log at warn level like below but add the exception stacktrace only with debug level + logger.warn(msg, ex); + } + else { + logger.warn(msg + ": " + ex); + } + } + } + + void logDestroyMethodCompletion(Method destroyMethod, boolean async) { + if (logger.isDebugEnabled()) { + logger.debug("Custom destroy method '" + destroyMethod.getName() + + "' on bean with name '" + this.beanName + "' completed" + (async ? " asynchronously" : "")); + } + } + /** * Serializes a copy of the state of this class, @@ -445,4 +485,59 @@ private static List filterPostProcessors( return filteredPostProcessors; } + + /** + * Inner class to avoid a hard dependency on the Reactive Streams API at runtime. + */ + private class ReactiveDestroyMethodHandler { + + public boolean await(Method destroyMethod, Object returnValue) throws InterruptedException { + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnValue.getClass()); + if (adapter != null) { + CountDownLatch latch = new CountDownLatch(1); + adapter.toPublisher(returnValue).subscribe(new DestroyMethodSubscriber(destroyMethod, latch)); + latch.await(); + return true; + } + return false; + } + } + + + /** + * Reactive Streams Subscriber for destroy method completion. + */ + private class DestroyMethodSubscriber implements Subscriber { + + private final Method destroyMethod; + + private final CountDownLatch latch; + + public DestroyMethodSubscriber(Method destroyMethod, CountDownLatch latch) { + this.destroyMethod = destroyMethod; + this.latch = latch; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Integer.MAX_VALUE); + } + + @Override + public void onNext(Object o) { + } + + @Override + public void onError(Throwable t) { + this.latch.countDown(); + logDestroyMethodException(this.destroyMethod, t); + } + + @Override + public void onComplete() { + this.latch.countDown(); + logDestroyMethodCompletion(this.destroyMethod, true); + } + } + } 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 72644917ea5c..bd19a2f4fc41 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 @@ -73,13 +73,17 @@ protected Class getTypeForFactoryBean(FactoryBean factoryBean) { */ ResolvableType getTypeForFactoryBeanFromAttributes(AttributeAccessor attributes) { Object attribute = attributes.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); + if (attribute == null) { + return ResolvableType.NONE; + } if (attribute instanceof ResolvableType resolvableType) { return resolvableType; } if (attribute instanceof Class clazz) { return ResolvableType.forClass(clazz); } - return ResolvableType.NONE; + throw new IllegalArgumentException("Invalid value type for attribute '" + + FactoryBean.OBJECT_TYPE_ATTRIBUTE + "': " + attribute.getClass().getName()); } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java index 3a3c84b11a1a..7d367cfd71aa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,22 +102,6 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc } } } - else { - // Pre-existing target type: In case of a generic FactoryBean type, - // unwrap nested generic type when matching a non-FactoryBean type. - Class resolvedClass = targetType.resolve(); - if (resolvedClass != null && FactoryBean.class.isAssignableFrom(resolvedClass)) { - Class typeToBeMatched = dependencyType.resolve(); - if (typeToBeMatched != null && !FactoryBean.class.isAssignableFrom(typeToBeMatched)) { - targetType = targetType.getGeneric(); - if (descriptor.fallbackMatchAllowed()) { - // Matching the Class-based type determination for FactoryBean - // objects in the lazy-determination getType code path below. - targetType = ResolvableType.forClass(targetType.resolve()); - } - } - } - } } if (targetType == null) { @@ -144,6 +128,23 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc if (cacheType) { rbd.targetType = targetType; } + + // Pre-declared target type: In case of a generic FactoryBean type, + // unwrap nested generic type when matching a non-FactoryBean type. + Class targetClass = targetType.resolve(); + if (targetClass != null && FactoryBean.class.isAssignableFrom(targetClass)) { + Class classToMatch = dependencyType.resolve(); + if (classToMatch != null && !FactoryBean.class.isAssignableFrom(classToMatch) && + !classToMatch.isAssignableFrom(targetClass)) { + targetType = targetType.getGeneric(); + if (descriptor.fallbackMatchAllowed()) { + // Matching the Class-based type determination for FactoryBean + // objects in the lazy-determination getType code path above. + targetType = ResolvableType.forClass(targetType.resolve()); + } + } + } + if (descriptor.fallbackMatchAllowed() && (targetType.hasUnresolvableGenerics() || targetType.resolve() == Properties.class)) { // Fallback matches allow unresolvable generics, e.g. plain HashMap to Map; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java index 8d930b357395..22e65bc12be5 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 @@ -83,6 +83,7 @@ public V get(RegisteredBean registeredBean) throws Exception { return after.applyWithException(registeredBean, InstanceSupplier.this.get(registeredBean)); } @Override + @Nullable public Method getFactoryMethod() { return InstanceSupplier.this.getFactoryMethod(); } @@ -126,6 +127,7 @@ public T get(RegisteredBean registeredBean) throws Exception { return supplier.getWithException(); } @Override + @Nullable public Method getFactoryMethod() { return factoryMethod; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java index c895734f0ec3..6670d5116f22 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,27 @@ package org.springframework.beans.factory.support; +import java.lang.reflect.Method; + import org.springframework.util.ClassUtils; /** - * Descriptor for a {@link java.lang.reflect.Method Method} which holds a + * Descriptor for a {@link Method Method} which holds a * reference to the method's {@linkplain #declaringClass declaring class}, * {@linkplain #methodName name}, and {@linkplain #parameterTypes parameter types}. * + * @author Sam Brannen + * @since 6.0.11 * @param declaringClass the method's declaring class * @param methodName the name of the method * @param parameterTypes the types of parameters accepted by the method - * @author Sam Brannen - * @since 6.0.11 */ record MethodDescriptor(Class declaringClass, String methodName, Class... parameterTypes) { /** * Create a {@link MethodDescriptor} for the supplied bean class and method name. *

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

If the method name is fully qualified, this utility will parse the * method name and its declaring class from the qualified method name and then diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java index e0880d60cbc4..d250320dded4 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 @@ -113,9 +113,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(this.methodName); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.source); - return hashCode; + return ObjectUtils.nullSafeHash(this.methodName, this.source); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java index 8b80359352d3..f59b5cb2643f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package org.springframework.beans.factory.support; import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Supplier; @@ -41,6 +43,8 @@ * In the case of inner-beans, the bean name may have been generated. * * @author Phillip Webb + * @author Stephane Nicoll + * @author Juergen Hoeller * @since 6.0 */ public final class RegisteredBean { @@ -206,12 +210,32 @@ public RegisteredBean getParent() { /** * Resolve the constructor or factory method to use for this bean. * @return the {@link java.lang.reflect.Constructor} or {@link java.lang.reflect.Method} + * @deprecated in favor of {@link #resolveInstantiationDescriptor()} */ + @Deprecated(since = "6.1.7") public Executable resolveConstructorOrFactoryMethod() { return new ConstructorResolver((AbstractAutowireCapableBeanFactory) getBeanFactory()) .resolveConstructorOrFactoryMethod(getBeanName(), getMergedBeanDefinition()); } + /** + * Resolve the {@linkplain InstantiationDescriptor descriptor} to use to + * instantiate this bean. It defines the {@link java.lang.reflect.Constructor} + * or {@link java.lang.reflect.Method} to use as well as additional metadata. + * @since 6.1.7 + */ + public InstantiationDescriptor resolveInstantiationDescriptor() { + Executable executable = resolveConstructorOrFactoryMethod(); + if (executable instanceof Method method && !Modifier.isStatic(method.getModifiers())) { + String factoryBeanName = getMergedBeanDefinition().getFactoryBeanName(); + if (factoryBeanName != null && this.beanFactory.containsBean(factoryBeanName)) { + return new InstantiationDescriptor(executable, + this.beanFactory.getMergedBeanDefinition(factoryBeanName).getResolvableType().toClass()); + } + } + return new InstantiationDescriptor(executable, executable.getDeclaringClass()); + } + /** * Resolve an autowired argument. * @param descriptor the descriptor for the dependency (field/method/constructor) @@ -238,6 +262,24 @@ public String toString() { } + /** + * Descriptor for how a bean should be instantiated. While the {@code targetClass} + * is usually the declaring class of the {@code executable} (in case of a constructor + * or a locally declared factory method), there are cases where retaining the actual + * concrete class is necessary (e.g. for an inherited factory method). + * @since 6.1.7 + * @param executable the {@link Executable} ({@link java.lang.reflect.Constructor} + * or {@link java.lang.reflect.Method}) to invoke + * @param targetClass the target {@link Class} of the executable + */ + public record InstantiationDescriptor(Executable executable, Class targetClass) { + + public InstantiationDescriptor(Executable executable) { + this(executable, executable.getDeclaringClass()); + } + } + + /** * Resolver used to obtain inner-bean details. */ 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 b88f1aa14f1d..feae33613fb6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -277,6 +277,7 @@ public RootBeanDefinition(RootBeanDefinition original) { @Override + @Nullable public String getParentName() { return null; } @@ -384,13 +385,28 @@ public ResolvableType getResolvableType() { /** * Determine preferred constructors to use for default construction, if any. * Constructor arguments will be autowired if necessary. + *

As of 6.1, the default implementation of this method takes the + * {@link #PREFERRED_CONSTRUCTORS_ATTRIBUTE} attribute into account. + * Subclasses are encouraged to preserve this through a {@code super} call, + * either before or after their own preferred constructor determination. * @return one or more preferred constructors, or {@code null} if none * (in which case the regular no-arg default constructor will be called) * @since 5.1 */ @Nullable public Constructor[] getPreferredConstructors() { - return null; + Object attribute = getAttribute(PREFERRED_CONSTRUCTORS_ATTRIBUTE); + if (attribute == null) { + return null; + } + if (attribute instanceof Constructor constructor) { + return new Constructor[] {constructor}; + } + if (attribute instanceof Constructor[]) { + return (Constructor[]) attribute; + } + throw new IllegalArgumentException("Invalid value type for attribute '" + + PREFERRED_CONSTRUCTORS_ATTRIBUTE + "': " + attribute.getClass().getName()); } /** @@ -623,7 +639,7 @@ boolean hasAnyExternallyManagedDestroyMethod(String destroyMethod) { } } - private static boolean hasAnyExternallyManagedMethod(Set candidates, String methodName) { + private static boolean hasAnyExternallyManagedMethod(@Nullable Set candidates, String methodName) { if (candidates != null) { for (String candidate : candidates) { int indexOfDot = candidate.lastIndexOf('.'); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java index 9efbc3b9b8c2..49c38d7e7389 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ * * @author Rod Johnson * @author Juergen Hoeller + * @author Stephane Nicoll * @since 1.1 */ public class SimpleInstantiationStrategy implements InstantiationStrategy { @@ -54,12 +55,18 @@ public static Method getCurrentlyInvokedFactoryMethod() { } /** - * Set the factory method currently being invoked or {@code null} to reset. + * Set the factory method currently being invoked or {@code null} to remove + * the current value, if any. * @param method the factory method currently being invoked or {@code null} * @since 6.0 */ public static void setCurrentlyInvokedFactoryMethod(@Nullable Method method) { - currentlyInvokedFactoryMethod.set(method); + if (method != null) { + currentlyInvokedFactoryMethod.set(method); + } + else { + currentlyInvokedFactoryMethod.remove(); + } } @@ -71,7 +78,7 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean synchronized (bd.constructorArgumentLock) { constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse == null) { - final Class clazz = bd.getBeanClass(); + Class clazz = bd.getBeanClass(); if (clazz.isInterface()) { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } @@ -104,7 +111,7 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - final Constructor ctor, Object... args) { + Constructor ctor, Object... args) { if (!bd.hasMethodOverrides()) { return BeanUtils.instantiateClass(ctor, args); @@ -128,14 +135,14 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - @Nullable Object factoryBean, final Method factoryMethod, Object... args) { + @Nullable Object factoryBean, Method factoryMethod, Object... args) { try { ReflectionUtils.makeAccessible(factoryMethod); - Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get(); + Method priorInvokedFactoryMethod = getCurrentlyInvokedFactoryMethod(); try { - currentlyInvokedFactoryMethod.set(factoryMethod); + setCurrentlyInvokedFactoryMethod(factoryMethod); Object result = factoryMethod.invoke(factoryBean, args); if (result == null) { result = new NullBean(); @@ -143,15 +150,15 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean return result; } finally { - if (priorInvokedFactoryMethod != null) { - currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod); - } - else { - currentlyInvokedFactoryMethod.remove(); - } + setCurrentlyInvokedFactoryMethod(priorInvokedFactoryMethod); } } catch (IllegalArgumentException ex) { + if (factoryBean != null && !factoryMethod.getDeclaringClass().isAssignableFrom(factoryBean.getClass())) { + throw new BeanInstantiationException(factoryMethod, + "Illegal factory instance for factory method '" + factoryMethod.getName() + "'; " + + "instance: " + factoryBean.getClass().getName(), ex); + } throw new BeanInstantiationException(factoryMethod, "Illegal arguments to factory method '" + factoryMethod.getName() + "'; " + "args: " + StringUtils.arrayToCommaDelimitedString(args), ex); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java index 4fded794a043..e93b7da2e359 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 @@ -232,11 +232,13 @@ public boolean isTypeMatch(String name, @Nullable Class typeToMatch) throws N } @Override + @Nullable public Class getType(String name) throws NoSuchBeanDefinitionException { return getType(name, true); } @Override + @Nullable public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { String beanName = BeanFactoryUtils.transformedBeanName(name); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java index b8e2935a9c46..0e556b94bc8f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,9 +126,10 @@ protected void doRegisterBeanDefinitions(Element root) { // then ultimately reset this.delegate back to its original (parent) reference. // this behavior emulates a stack of delegates without actually necessitating one. BeanDefinitionParserDelegate parent = this.delegate; - this.delegate = createDelegate(getReaderContext(), root, parent); + BeanDefinitionParserDelegate current = createDelegate(getReaderContext(), root, parent); + this.delegate = current; - if (this.delegate.isDefaultNamespace(root)) { + if (current.isDefaultNamespace(root)) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { String[] specifiedProfiles = StringUtils.tokenizeToStringArray( @@ -146,7 +147,7 @@ protected void doRegisterBeanDefinitions(Element root) { } preProcessXml(root); - parseBeanDefinitions(root, this.delegate); + parseBeanDefinitions(root, current); postProcessXml(root); this.delegate = parent; 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 1804e3b48168..12232bddf71c 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 @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.HashSet; +import java.util.Map; import java.util.Set; import javax.xml.parsers.ParserConfigurationException; @@ -40,7 +41,6 @@ import org.springframework.beans.factory.parsing.SourceExtractor; import org.springframework.beans.factory.support.AbstractBeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.core.Constants; import org.springframework.core.NamedThreadLocal; import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; @@ -68,6 +68,7 @@ * @author Juergen Hoeller * @author Rob Harrop * @author Chris Beams + * @author Sam Brannen * @since 26.11.2003 * @see #setDocumentReaderClass * @see BeanDefinitionDocumentReader @@ -99,8 +100,16 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD; - /** Constants instance for this class. */ - private static final Constants constants = new Constants(XmlBeanDefinitionReader.class); + /** + * Map of constant names to constant values for the validation constants defined + * in this class. + */ + private static final Map constants = Map.of( + "VALIDATION_NONE", VALIDATION_NONE, + "VALIDATION_AUTO", VALIDATION_AUTO, + "VALIDATION_DTD", VALIDATION_DTD, + "VALIDATION_XSD", VALIDATION_XSD + ); private int validationMode = VALIDATION_AUTO; @@ -127,13 +136,8 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector(); - private final ThreadLocal> resourcesCurrentlyBeingLoaded = - new NamedThreadLocal<>("XML bean definition resources currently being loaded"){ - @Override - protected Set initialValue() { - return new HashSet<>(4); - } - }; + private final ThreadLocal> resourcesCurrentlyBeingLoaded = NamedThreadLocal.withInitial( + "XML bean definition resources currently being loaded", () -> new HashSet<>(4)); /** @@ -163,7 +167,10 @@ public void setValidating(boolean validating) { * @see #setValidationMode */ public void setValidationModeName(String validationModeName) { - setValidationMode(constants.asNumber(validationModeName).intValue()); + Assert.hasText(validationModeName, "'validationModeName' must not be null or blank"); + Integer validationMode = constants.get(validationModeName); + Assert.notNull(validationMode, "Only validation mode constants allowed"); + this.validationMode = validationMode; } /** @@ -173,6 +180,8 @@ public void setValidationModeName(String validationModeName) { * activate schema namespace support explicitly: see {@link #setNamespaceAware}. */ public void setValidationMode(int validationMode) { + Assert.isTrue(constants.containsValue(validationMode), + "Only values of validation mode constants allowed"); this.validationMode = validationMode; } 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..c2c415deab55 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,13 @@ * @author Juergen Hoeller * @since 26.05.2003 * @see java.util.Locale - * @see org.springframework.util.StringUtils#parseLocaleString + * @see org.springframework.util.StringUtils#parseLocale */ public class LocaleEditor extends PropertyEditorSupport { @Override public void setAsText(String text) { - setValue(StringUtils.parseLocaleString(text)); + setValue(StringUtils.parseLocale(text)); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index 13226e6ca0db..70e348f09d2b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,8 +78,10 @@ public void setAsText(String text) throws IllegalArgumentException { if (nioPathCandidate && !text.startsWith("/")) { try { URI uri = ResourceUtils.toURI(text); - if (uri.getScheme() != null) { - nioPathCandidate = false; + String scheme = uri.getScheme(); + if (scheme != null) { + // No NIO candidate except for "C:" style drive letters + nioPathCandidate = (scheme.length() == 1); // Let's try NIO file system providers via Paths.get(URI) setValue(Paths.get(uri).normalize()); return; @@ -90,9 +92,9 @@ public void setAsText(String text) throws IllegalArgumentException { // a file prefix (let's try as Spring resource location) nioPathCandidate = !text.startsWith(ResourceUtils.FILE_URL_PREFIX); } - catch (FileSystemNotFoundException ex) { - // URI scheme not registered for NIO (let's try URL - // protocol handlers via Spring's resource mechanism). + catch (FileSystemNotFoundException | IllegalArgumentException ex) { + // URI scheme not registered for NIO or not meeting Paths requirements: + // let's try URL protocol handlers via Spring's resource mechanism. } } @@ -109,7 +111,13 @@ else if (nioPathCandidate && !resource.exists()) { setValue(resource.getFile().toPath()); } catch (IOException ex) { - throw new IllegalArgumentException("Failed to retrieve file for " + resource, ex); + String msg = "Could not resolve \"" + text + "\" to 'java.nio.file.Path' for " + resource + ": " + + ex.getMessage(); + if (nioPathCandidate) { + msg += " - In case of ambiguity, consider adding the 'file:' prefix for an explicit reference " + + "to a file system resource of the same name: \"file:" + text + "\""; + } + throw new IllegalArgumentException(msg); } } } diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt index ed6f5a66b48c..1ef029900082 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-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,17 @@ import org.springframework.core.ResolvableType /** * Extension for [BeanFactory.getBean] providing a `getBean()` variant. + * This extension is not subject to type erasure and retains actual generic type arguments. * * @author Sebastien Deleuze * @since 5.0 */ -inline fun BeanFactory.getBean(): T = getBean(T::class.java) +inline fun BeanFactory.getBean(): T = + getBeanProvider().getObject() /** * Extension for [BeanFactory.getBean] providing a `getBean("foo")` variant. + * Like the original Java method, this extension is subject to type erasure. * * @see BeanFactory.getBean(String, Class) * @author Sebastien Deleuze @@ -40,13 +43,14 @@ inline fun BeanFactory.getBean(name: String): T = /** * Extension for [BeanFactory.getBean] providing a `getBean(arg1, arg2)` variant. + * This extension is not subject to type erasure and retains actual generic type arguments. * * @see BeanFactory.getBean(Class, Object...) * @author Sebastien Deleuze * @since 5.0 */ inline fun BeanFactory.getBean(vararg args:Any): T = - getBean(T::class.java, *args) + getBeanProvider().getObject(*args) /** * Extension for [BeanFactory.getBeanProvider] providing a `getBeanProvider()` variant. diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index 29487423b675..dd50e8c117c1 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -139,12 +139,14 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isReadableProperty("list")).isTrue(); assertThat(accessor.isReadableProperty("set")).isTrue(); assertThat(accessor.isReadableProperty("map")).isTrue(); + assertThat(accessor.isReadableProperty("myTestBeans")).isTrue(); assertThat(accessor.isReadableProperty("xxx")).isFalse(); assertThat(accessor.isWritableProperty("array")).isTrue(); assertThat(accessor.isWritableProperty("list")).isTrue(); assertThat(accessor.isWritableProperty("set")).isTrue(); assertThat(accessor.isWritableProperty("map")).isTrue(); + assertThat(accessor.isWritableProperty("myTestBeans")).isTrue(); assertThat(accessor.isWritableProperty("xxx")).isFalse(); assertThat(accessor.isReadableProperty("array[0]")).isTrue(); @@ -159,6 +161,8 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue(); assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue(); assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue(); + assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse(); assertThat(accessor.isReadableProperty("array[key1]")).isFalse(); assertThat(accessor.isWritableProperty("array[0]")).isTrue(); @@ -173,6 +177,8 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue(); assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue(); assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue(); + assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse(); assertThat(accessor.isWritableProperty("array[key1]")).isFalse(); } @@ -288,7 +294,7 @@ void setNestedProperty() { } @Test - void setNestedPropertyPolymorphic() throws Exception { + void setNestedPropertyPolymorphic() { ITestBean target = new TestBean("rod", 31); ITestBean kerry = new Employee(); @@ -310,7 +316,7 @@ void setNestedPropertyPolymorphic() throws Exception { } @Test - void setAnotherNestedProperty() throws Exception { + void setAnotherNestedProperty() { ITestBean target = new TestBean("rod", 31); ITestBean kerry = new TestBean("kerry", 0); @@ -380,7 +386,7 @@ void setPropertyIntermediatePropertyIsNull() { } @Test - void setAnotherPropertyIntermediatePropertyIsNull() throws Exception { + void setAnotherPropertyIntermediatePropertyIsNull() { ITestBean target = new TestBean("rod", 31); AbstractPropertyAccessor accessor = createAccessor(target); assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> @@ -408,7 +414,7 @@ void setPropertyIntermediateListIsNullWithAutoGrow() { Map map = new HashMap<>(); map.put("favoriteNumber", "9"); accessor.setPropertyValue("list[0]", map); - assertThat(target.list.get(0)).isEqualTo(map); + assertThat(target.list).element(0).isEqualTo(map); } @Test @@ -546,7 +552,7 @@ public void setValue(Object value) { } @Test - void setStringPropertyWithCustomEditor() throws Exception { + void setStringPropertyWithCustomEditor() { TestBean target = new TestBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { @@ -717,7 +723,7 @@ void setWildcardEnumProperty() { } @Test - void setPropertiesProperty() throws Exception { + void setPropertiesProperty() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setPropertyValue("name", "ptest"); @@ -735,7 +741,7 @@ void setPropertiesProperty() throws Exception { } @Test - void setStringArrayProperty() throws Exception { + void setStringArrayProperty() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); @@ -760,7 +766,7 @@ void setStringArrayProperty() throws Exception { } @Test - void setStringArrayPropertyWithCustomStringEditor() throws Exception { + void setStringArrayPropertyWithCustomStringEditor() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(String.class, "stringArray", new PropertyEditorSupport() { @@ -789,7 +795,7 @@ public void setAsText(String text) { } @Test - void setStringArrayPropertyWithStringSplitting() throws Exception { + void setStringArrayPropertyWithStringSplitting() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.useConfigValueEditors(); @@ -798,7 +804,7 @@ void setStringArrayPropertyWithStringSplitting() throws Exception { } @Test - void setStringArrayPropertyWithCustomStringDelimiter() throws Exception { + void setStringArrayPropertyWithCustomStringDelimiter() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.registerCustomEditor(String[].class, "stringArray", new StringArrayPropertyEditor("-")); @@ -807,7 +813,7 @@ void setStringArrayPropertyWithCustomStringDelimiter() throws Exception { } @Test - void setStringArrayWithAutoGrow() throws Exception { + void setStringArrayWithAutoGrow() { StringArrayBean target = new StringArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setAutoGrowNestedPaths(true); @@ -881,7 +887,7 @@ public void setAsText(String text) { } @Test - void setIntArrayPropertyWithStringSplitting() throws Exception { + void setIntArrayPropertyWithStringSplitting() { PropsTester target = new PropsTester(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.useConfigValueEditors(); @@ -936,7 +942,7 @@ public void setValue(Object value) { } @Test - void setPrimitiveArrayPropertyWithAutoGrow() throws Exception { + void setPrimitiveArrayPropertyWithAutoGrow() { PrimitiveArrayBean target = new PrimitiveArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); accessor.setAutoGrowNestedPaths(true); @@ -1137,12 +1143,12 @@ void setCollectionPropertyWithStringValueAndCustomEditor() { assertThat(target.getSet()).hasSize(1); assertThat(target.getSet().contains("set1")).isTrue(); assertThat(target.getSortedSet()).hasSize(1); - assertThat(target.getSortedSet().contains("sortedSet1")).isTrue(); + assertThat(target.getSortedSet()).contains("sortedSet1"); assertThat(target.getList()).hasSize(1); - assertThat(target.getList().contains("list1")).isTrue(); + assertThat(target.getList()).contains("list1"); accessor.setPropertyValue("list", Collections.singletonList("list1 ")); - assertThat(target.getList().contains("list1")).isTrue(); + assertThat(target.getList()).contains("list1"); } @Test @@ -1388,6 +1394,7 @@ void getAndSetIndexedProperties() { assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8"); assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8"); assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8"); + assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZ"); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("array[0].name", "name5"); @@ -1401,6 +1408,7 @@ void getAndSetIndexedProperties() { pvs.add("map[key4][0].name", "nameA"); pvs.add("map[key4][1].name", "nameB"); pvs.add("map[key5[foo]].name", "name10"); + pvs.add("myTestBeans[0].name", "nameZZ"); accessor.setPropertyValues(pvs); assertThat(tb0.getName()).isEqualTo("name5"); assertThat(tb1.getName()).isEqualTo("name4"); @@ -1419,6 +1427,7 @@ void getAndSetIndexedProperties() { assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA"); assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB"); assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10"); + assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZZ"); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java deleted file mode 100644 index 26bd33066e19..000000000000 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java +++ /dev/null @@ -1,55 +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; - -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Rod Johnson - * @author Chris Beams - */ -public abstract class AbstractPropertyValuesTests { - - /** - * Must contain: forname=Tony surname=Blair age=50 - */ - protected void doTestTony(PropertyValues pvs) { - assertThat(pvs.getPropertyValues()).as("Contains 3").hasSize(3); - assertThat(pvs.contains("forname")).as("Contains forname").isTrue(); - assertThat(pvs.contains("surname")).as("Contains surname").isTrue(); - assertThat(pvs.contains("age")).as("Contains age").isTrue(); - assertThat(!pvs.contains("tory")).as("Doesn't contain tory").isTrue(); - - PropertyValue[] ps = pvs.getPropertyValues(); - Map m = new HashMap<>(); - m.put("forname", "Tony"); - m.put("surname", "Blair"); - m.put("age", "50"); - for (PropertyValue element : ps) { - Object val = m.get(element.getName()); - assertThat(val).as("Can't have unexpected value").isNotNull(); - assertThat(val instanceof String).as("Val i string").isTrue(); - assertThat(val.equals(element.getValue())).as("val matches expected").isTrue(); - m.remove(element.getName()); - } - assertThat(m).as("Map size is 0").isEmpty(); - } - -} diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java index 0d75f7c37187..09edd5cf1a63 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -41,6 +42,8 @@ import org.springframework.beans.testfixture.beans.DerivedTestBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; import org.springframework.lang.Nullable; @@ -51,7 +54,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; /** - * Unit tests for {@link BeanUtils}. + * Tests for {@link BeanUtils}. * * @author Juergen Hoeller * @author Rob Harrop @@ -144,7 +147,7 @@ void beanPropertyIsArray() { for (PropertyDescriptor descriptor : descriptors) { if ("containedBeans".equals(descriptor.getName())) { assertThat(descriptor.getPropertyType().isArray()).as("Property should be an array").isTrue(); - assertThat(ContainedBean.class).isEqualTo(descriptor.getPropertyType().getComponentType()); + assertThat(ContainedBean.class).isEqualTo(descriptor.getPropertyType().componentType()); } } } @@ -321,12 +324,13 @@ void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() thro Order original = new Order("test", List.of("foo", "bar")); // Create a Proxy that loses the generic type information for the getLineItems() method. - OrderSummary proxy = proxyOrder(original); + OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {OrderSummary.class}, new OrderInvocationHandler(original)); assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString()) - .contains("java.util.List"); + .contains("java.util.List"); assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString()) - .contains("java.util.List") - .doesNotContain(""); + .contains("java.util.List") + .doesNotContain(""); // Ensure that our custom Proxy works as expected. assertThat(proxy.getId()).isEqualTo("test"); @@ -339,6 +343,23 @@ void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() thro assertThat(target.getLineItems()).containsExactly("foo", "bar"); } + @Test // gh-32888 + public void copyPropertiesWithGenericCglibClass() { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(User.class); + enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args)); + User user = (User) enhancer.create(); + user.setId(1); + user.setName("proxy"); + user.setAddress("addr"); + + User target = new User(); + BeanUtils.copyProperties(user, target); + assertThat(target.getId()).isEqualTo(user.getId()); + assertThat(target.getName()).isEqualTo(user.getName()); + assertThat(target.getAddress()).isEqualTo(user.getAddress()); + } + @Test void copyPropertiesWithEditable() throws Exception { TestBean tb = new TestBean(); @@ -470,7 +491,8 @@ void spr6063() { @ValueSource(classes = { boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class, Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, - DayOfWeek.class, String.class, LocalDateTime.class, Date.class, URI.class, URL.class, Locale.class, Class.class + DayOfWeek.class, String.class, LocalDateTime.class, Date.class, UUID.class, URI.class, URL.class, + Locale.class, Class.class }) void isSimpleValueType(Class type) { assertThat(BeanUtils.isSimpleValueType(type)).as("Type [" + type.getName() + "] should be a simple value type").isTrue(); @@ -486,8 +508,8 @@ void isNotSimpleValueType(Class type) { @ValueSource(classes = { boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class, Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, - DayOfWeek.class, String.class, LocalDateTime.class, Date.class, URI.class, URL.class, Locale.class, Class.class, - boolean[].class, Boolean[].class, LocalDateTime[].class, Date[].class + DayOfWeek.class, String.class, LocalDateTime.class, Date.class, UUID.class, URI.class, URL.class, + Locale.class, Class.class, boolean[].class, Boolean[].class, LocalDateTime[].class, Date[].class }) void isSimpleProperty(Class type) { assertThat(BeanUtils.isSimpleProperty(type)).as("Type [" + type.getName() + "] should be a simple property").isTrue(); @@ -518,6 +540,7 @@ public void setNumber(Number number) { } } + @SuppressWarnings("unused") private static class IntegerHolder { @@ -532,6 +555,7 @@ public void setNumber(Integer number) { } } + @SuppressWarnings("unused") private static class WildcardListHolder1 { @@ -546,6 +570,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class WildcardListHolder2 { @@ -560,6 +585,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class NumberUpperBoundedWildcardListHolder { @@ -574,6 +600,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class NumberListHolder { @@ -588,6 +615,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class IntegerListHolder1 { @@ -602,6 +630,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class IntegerListHolder2 { @@ -616,6 +645,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class LongListHolder { @@ -796,6 +826,7 @@ public void setValue(String aValue) { } } + private static class BeanWithNullableTypes { private Integer counter; @@ -826,6 +857,7 @@ public String getValue() { } } + private static class BeanWithPrimitiveTypes { private boolean flag; @@ -838,7 +870,6 @@ private static class BeanWithPrimitiveTypes { private char character; private String text; - @SuppressWarnings("unused") public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount, float floatCount, double doubleCount, char character, String text) { @@ -889,21 +920,22 @@ public char getCharacter() { public String getText() { return text; } - } + private static class PrivateBeanWithPrivateConstructor { private PrivateBeanWithPrivateConstructor() { } } + @SuppressWarnings("unused") private static class Order { private String id; - private List lineItems; + private List lineItems; Order() { } @@ -935,6 +967,7 @@ public String toString() { } } + private interface OrderSummary { String getId(); @@ -943,17 +976,10 @@ private interface OrderSummary { } - private OrderSummary proxyOrder(Order order) { - return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(), - new Class[] { OrderSummary.class }, new OrderInvocationHandler(order)); - } - - private static class OrderInvocationHandler implements InvocationHandler { private final Order order; - OrderInvocationHandler(Order order) { this.order = order; } @@ -971,4 +997,46 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } + + private static class GenericBaseModel { + + private T id; + + private String name; + + public T getId() { + return id; + } + + public void setId(T id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + private static class User extends GenericBaseModel { + + private String address; + + public User() { + super(); + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java index 57820c1e8fdf..825ff1988a12 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -30,7 +31,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class BeanWrapperAutoGrowingTests { +class BeanWrapperAutoGrowingTests { private final Bean bean = new Bean(); @@ -38,72 +39,72 @@ public class BeanWrapperAutoGrowingTests { @BeforeEach - public void setup() { + void setup() { wrapper.setAutoGrowNestedPaths(true); } @Test - public void getPropertyValueNullValueInNestedPath() { + void getPropertyValueNullValueInNestedPath() { assertThat(wrapper.getPropertyValue("nested.prop")).isNull(); } @Test - public void setPropertyValueNullValueInNestedPath() { + void setPropertyValueNullValueInNestedPath() { wrapper.setPropertyValue("nested.prop", "test"); assertThat(bean.getNested().getProp()).isEqualTo("test"); } @Test - public void getPropertyValueNullValueInNestedPathNoDefaultConstructor() { + void getPropertyValueNullValueInNestedPathNoDefaultConstructor() { assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> wrapper.getPropertyValue("nestedNoConstructor.prop")); } @Test - public void getPropertyValueAutoGrowArray() { - assertNotNull(wrapper.getPropertyValue("array[0]")); + void getPropertyValueAutoGrowArray() { + assertThat(wrapper.getPropertyValue("array[0]")).isNotNull(); assertThat(bean.getArray()).hasSize(1); assertThat(bean.getArray()[0]).isInstanceOf(Bean.class); } @Test - public void setPropertyValueAutoGrowArray() { + void setPropertyValueAutoGrowArray() { wrapper.setPropertyValue("array[0].prop", "test"); assertThat(bean.getArray()[0].getProp()).isEqualTo("test"); } @Test - public void getPropertyValueAutoGrowArrayBySeveralElements() { - assertNotNull(wrapper.getPropertyValue("array[4]")); + void getPropertyValueAutoGrowArrayBySeveralElements() { + assertThat(wrapper.getPropertyValue("array[4]")).isNotNull(); assertThat(bean.getArray()).hasSize(5); assertThat(bean.getArray()[0]).isInstanceOf(Bean.class); assertThat(bean.getArray()[1]).isInstanceOf(Bean.class); assertThat(bean.getArray()[2]).isInstanceOf(Bean.class); assertThat(bean.getArray()[3]).isInstanceOf(Bean.class); assertThat(bean.getArray()[4]).isInstanceOf(Bean.class); - assertNotNull(wrapper.getPropertyValue("array[0]")); - assertNotNull(wrapper.getPropertyValue("array[1]")); - assertNotNull(wrapper.getPropertyValue("array[2]")); - assertNotNull(wrapper.getPropertyValue("array[3]")); + assertThat(wrapper.getPropertyValue("array[0]")).isNotNull(); + assertThat(wrapper.getPropertyValue("array[1]")).isNotNull(); + assertThat(wrapper.getPropertyValue("array[2]")).isNotNull(); + assertThat(wrapper.getPropertyValue("array[3]")).isNotNull(); } @Test - public void getPropertyValueAutoGrow2dArray() { + void getPropertyValueAutoGrow2dArray() { assertThat(wrapper.getPropertyValue("multiArray[0][0]")).isNotNull(); assertThat(bean.getMultiArray()[0]).hasSize(1); assertThat(bean.getMultiArray()[0][0]).isInstanceOf(Bean.class); } @Test - public void getPropertyValueAutoGrow3dArray() { + void getPropertyValueAutoGrow3dArray() { assertThat(wrapper.getPropertyValue("threeDimensionalArray[1][2][3]")).isNotNull(); assertThat(bean.getThreeDimensionalArray()[1]).hasNumberOfRows(3); assertThat(bean.getThreeDimensionalArray()[1][2][3]).isInstanceOf(Bean.class); } @Test - public void setPropertyValueAutoGrow2dArray() { + void setPropertyValueAutoGrow2dArray() { Bean newBean = new Bean(); newBean.setProp("enigma"); wrapper.setPropertyValue("multiArray[2][3]", newBean); @@ -113,7 +114,7 @@ public void setPropertyValueAutoGrow2dArray() { } @Test - public void setPropertyValueAutoGrow3dArray() { + void setPropertyValueAutoGrow3dArray() { Bean newBean = new Bean(); newBean.setProp("enigma"); wrapper.setPropertyValue("threeDimensionalArray[2][3][4]", newBean); @@ -123,69 +124,79 @@ public void setPropertyValueAutoGrow3dArray() { } @Test - public void getPropertyValueAutoGrowList() { - assertNotNull(wrapper.getPropertyValue("list[0]")); + void getPropertyValueAutoGrowList() { + assertThat(wrapper.getPropertyValue("list[0]")).isNotNull(); assertThat(bean.getList()).hasSize(1); - assertThat(bean.getList().get(0)).isInstanceOf(Bean.class); + assertThat(bean.getList()).element(0).isInstanceOf(Bean.class); } @Test - public void setPropertyValueAutoGrowList() { + void setPropertyValueAutoGrowList() { wrapper.setPropertyValue("list[0].prop", "test"); assertThat(bean.getList().get(0).getProp()).isEqualTo("test"); } @Test - public void getPropertyValueAutoGrowListBySeveralElements() { - assertNotNull(wrapper.getPropertyValue("list[4]")); - assertThat(bean.getList()).hasSize(5); - assertThat(bean.getList().get(0)).isInstanceOf(Bean.class); - assertThat(bean.getList().get(1)).isInstanceOf(Bean.class); - assertThat(bean.getList().get(2)).isInstanceOf(Bean.class); - assertThat(bean.getList().get(3)).isInstanceOf(Bean.class); - assertThat(bean.getList().get(4)).isInstanceOf(Bean.class); - assertNotNull(wrapper.getPropertyValue("list[0]")); - assertNotNull(wrapper.getPropertyValue("list[1]")); - assertNotNull(wrapper.getPropertyValue("list[2]")); - assertNotNull(wrapper.getPropertyValue("list[3]")); + void getPropertyValueAutoGrowListBySeveralElements() { + assertThat(wrapper.getPropertyValue("list[4]")).isNotNull(); + assertThat(bean.getList()).hasSize(5).allSatisfy(entry -> + assertThat(entry).isInstanceOf(Bean.class)); + assertThat(wrapper.getPropertyValue("list[0]")).isNotNull(); + assertThat(wrapper.getPropertyValue("list[1]")).isNotNull(); + assertThat(wrapper.getPropertyValue("list[2]")).isNotNull(); + assertThat(wrapper.getPropertyValue("list[3]")).isNotNull(); } @Test - public void getPropertyValueAutoGrowListFailsAgainstLimit() { + void getPropertyValueAutoGrowListFailsAgainstLimit() { wrapper.setAutoGrowCollectionLimit(2); - assertThatExceptionOfType(InvalidPropertyException.class).isThrownBy(() -> - wrapper.getPropertyValue("list[4]")) - .withRootCauseInstanceOf(IndexOutOfBoundsException.class); + assertThatExceptionOfType(InvalidPropertyException.class) + .isThrownBy(() -> wrapper.getPropertyValue("list[4]")) + .withRootCauseInstanceOf(IndexOutOfBoundsException.class); + } + + @Test + void getPropertyValueAutoGrowNestedList() { + assertThat(wrapper.getPropertyValue("nestedList[0][0]")).isNotNull(); + assertThat(bean.getNestedList()).hasSize(1); + assertThat(bean.getNestedList().get(0)).singleElement().isInstanceOf(Bean.class); } @Test - public void getPropertyValueAutoGrowMultiDimensionalList() { - assertNotNull(wrapper.getPropertyValue("multiList[0][0]")); - assertThat(bean.getMultiList().get(0)).hasSize(1); - assertThat(bean.getMultiList().get(0).get(0)).isInstanceOf(Bean.class); + void getPropertyValueAutoGrowNestedNestedList() { + assertThat(wrapper.getPropertyValue("nestedNestedList[0][0][0]")).isNotNull(); + assertThat(bean.getNestedNestedList()).hasSize(1); + assertThat(bean.getNestedNestedList().get(0).get(0)).singleElement().isInstanceOf(Bean.class); } @Test - public void getPropertyValueAutoGrowListNotParameterized() { + void getPropertyValueAutoGrowListNotParameterized() { assertThatExceptionOfType(InvalidPropertyException.class).isThrownBy(() -> wrapper.getPropertyValue("listNotParameterized[0]")); } @Test - public void setPropertyValueAutoGrowMap() { + void setPropertyValueAutoGrowMap() { wrapper.setPropertyValue("map[A]", new Bean()); assertThat(bean.getMap().get("A")).isInstanceOf(Bean.class); } @Test - public void setNestedPropertyValueAutoGrowMap() { + void setPropertyValueAutoGrowMapNestedValue() { wrapper.setPropertyValue("map[A].nested", new Bean()); assertThat(bean.getMap().get("A").getNested()).isInstanceOf(Bean.class); } + @Test + void setPropertyValueAutoGrowNestedMapWithinMap() { + wrapper.setPropertyValue("nestedMap[A][B]", new Bean()); + assertThat(bean.getNestedMap().get("A").get("B")).isInstanceOf(Bean.class); + } - private static void assertNotNull(Object propertyValue) { - assertThat(propertyValue).isNotNull(); + @Test @Disabled // gh-32154 + void setPropertyValueAutoGrowNestedNestedMapWithinMap() { + wrapper.setPropertyValue("nestedNestedMap[A][B][C]", new Bean()); + assertThat(bean.getNestedNestedMap().get("A").get("B").get("C")).isInstanceOf(Bean.class); } @@ -206,12 +217,18 @@ public static class Bean { private List list; - private List> multiList; + private List> nestedList; + + private List>> nestedNestedList; private List listNotParameterized; private Map map; + private Map> nestedMap; + + private Map>> nestedNestedMap; + public String getProp() { return prop; } @@ -260,12 +277,20 @@ public void setList(List list) { this.list = list; } - public List> getMultiList() { - return multiList; + public List> getNestedList() { + return nestedList; + } + + public void setNestedList(List> nestedList) { + this.nestedList = nestedList; } - public void setMultiList(List> multiList) { - this.multiList = multiList; + public List>> getNestedNestedList() { + return nestedNestedList; + } + + public void setNestedNestedList(List>> nestedNestedList) { + this.nestedNestedList = nestedNestedList; } public NestedNoDefaultConstructor getNestedNoConstructor() { @@ -291,6 +316,22 @@ public Map getMap() { public void setMap(Map map) { this.map = map; } + + public Map> getNestedMap() { + return nestedMap; + } + + public void setNestedMap(Map> nestedMap) { + this.nestedMap = nestedMap; + } + + public Map>> getNestedNestedMap() { + return nestedNestedMap; + } + + public void setNestedNestedMap(Map>> nestedNestedMap) { + this.nestedNestedMap = nestedNestedMap; + } } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java index 3dad8d527a66..21c07a6d7688 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,10 +31,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class BeanWrapperEnumTests { +class BeanWrapperEnumTests { @Test - public void testCustomEnum() { + void testCustomEnum() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnum", "VALUE_1"); @@ -42,7 +42,7 @@ public void testCustomEnum() { } @Test - public void testCustomEnumWithNull() { + void testCustomEnumWithNull() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnum", null); @@ -50,7 +50,7 @@ public void testCustomEnumWithNull() { } @Test - public void testCustomEnumWithEmptyString() { + void testCustomEnumWithEmptyString() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnum", ""); @@ -58,7 +58,7 @@ public void testCustomEnumWithEmptyString() { } @Test - public void testCustomEnumArrayWithSingleValue() { + void testCustomEnumArrayWithSingleValue() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnumArray", "VALUE_1"); @@ -67,7 +67,7 @@ public void testCustomEnumArrayWithSingleValue() { } @Test - public void testCustomEnumArrayWithMultipleValues() { + void testCustomEnumArrayWithMultipleValues() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnumArray", new String[] {"VALUE_1", "VALUE_2"}); @@ -77,7 +77,7 @@ public void testCustomEnumArrayWithMultipleValues() { } @Test - public void testCustomEnumArrayWithMultipleValuesAsCsv() { + void testCustomEnumArrayWithMultipleValuesAsCsv() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnumArray", "VALUE_1,VALUE_2"); @@ -87,58 +87,58 @@ public void testCustomEnumArrayWithMultipleValuesAsCsv() { } @Test - public void testCustomEnumSetWithSingleValue() { + void testCustomEnumSetWithSingleValue() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnumSet", "VALUE_1"); assertThat(gb.getCustomEnumSet()).hasSize(1); - assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); + assertThat(gb.getCustomEnumSet()).contains(CustomEnum.VALUE_1); } @Test - public void testCustomEnumSetWithMultipleValues() { + void testCustomEnumSetWithMultipleValues() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnumSet", new String[] {"VALUE_1", "VALUE_2"}); assertThat(gb.getCustomEnumSet()).hasSize(2); - assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); - assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + assertThat(gb.getCustomEnumSet()).contains(CustomEnum.VALUE_1); + assertThat(gb.getCustomEnumSet()).contains(CustomEnum.VALUE_2); } @Test - public void testCustomEnumSetWithMultipleValuesAsCsv() { + void testCustomEnumSetWithMultipleValuesAsCsv() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnumSet", "VALUE_1,VALUE_2"); assertThat(gb.getCustomEnumSet()).hasSize(2); - assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); - assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + assertThat(gb.getCustomEnumSet()).contains(CustomEnum.VALUE_1); + assertThat(gb.getCustomEnumSet()).contains(CustomEnum.VALUE_2); } @Test - public void testCustomEnumSetWithGetterSetterMismatch() { + void testCustomEnumSetWithGetterSetterMismatch() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("customEnumSetMismatch", new String[] {"VALUE_1", "VALUE_2"}); assertThat(gb.getCustomEnumSet()).hasSize(2); - assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); - assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + assertThat(gb.getCustomEnumSet()).contains(CustomEnum.VALUE_1); + assertThat(gb.getCustomEnumSet()).contains(CustomEnum.VALUE_2); } @Test - public void testStandardEnumSetWithMultipleValues() { + void testStandardEnumSetWithMultipleValues() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setConversionService(new DefaultConversionService()); assertThat(gb.getStandardEnumSet()).isNull(); bw.setPropertyValue("standardEnumSet", new String[] {"VALUE_1", "VALUE_2"}); assertThat(gb.getStandardEnumSet()).hasSize(2); - assertThat(gb.getStandardEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); - assertThat(gb.getStandardEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + assertThat(gb.getStandardEnumSet()).contains(CustomEnum.VALUE_1); + assertThat(gb.getStandardEnumSet()).contains(CustomEnum.VALUE_2); } @Test - public void testStandardEnumSetWithAutoGrowing() { + void testStandardEnumSetWithAutoGrowing() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setAutoGrowNestedPaths(true); @@ -148,7 +148,7 @@ public void testStandardEnumSetWithAutoGrowing() { } @Test - public void testStandardEnumMapWithMultipleValues() { + void testStandardEnumMapWithMultipleValues() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setConversionService(new DefaultConversionService()); @@ -163,7 +163,7 @@ public void testStandardEnumMapWithMultipleValues() { } @Test - public void testStandardEnumMapWithAutoGrowing() { + void testStandardEnumMapWithAutoGrowing() { GenericBean gb = new GenericBean<>(); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setAutoGrowNestedPaths(true); @@ -174,7 +174,7 @@ public void testStandardEnumMapWithAutoGrowing() { } @Test - public void testNonPublicEnum() { + void testNonPublicEnum() { NonPublicEnumHolder holder = new NonPublicEnumHolder(); BeanWrapper bw = new BeanWrapperImpl(holder); bw.setPropertyValue("nonPublicEnum", "VALUE_1"); @@ -184,7 +184,7 @@ public void testNonPublicEnum() { enum NonPublicEnum { - VALUE_1, VALUE_2; + VALUE_1, VALUE_2 } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java index 6012ecfaa9cd..522e13c5b9b3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,10 @@ import org.springframework.beans.testfixture.beans.GenericSetOfIntegerBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.io.UrlResource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -55,8 +58,8 @@ void testGenericSet() { input.add("4"); input.add("5"); bw.setPropertyValue("integerSet", input); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getIntegerSet()).contains(4); + assertThat(gb.getIntegerSet()).contains(5); } @Test @@ -78,9 +81,9 @@ void testGenericSetWithConversionFailure() { BeanWrapper bw = new BeanWrapperImpl(gb); Set input = new HashSet<>(); input.add(new TestBean()); - assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> - bw.setPropertyValue("integerSet", input)) - .withMessageContaining("java.lang.Integer"); + assertThatExceptionOfType(TypeMismatchException.class) + .isThrownBy(() -> bw.setPropertyValue("integerSet", input)) + .withMessageContaining("java.lang.Integer"); } @Test @@ -91,8 +94,8 @@ void testGenericList() throws Exception { input.add("http://localhost:8080"); input.add("http://localhost:9090"); bw.setPropertyValue("resourceList", input); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + assertThat(gb.getResourceList()).containsExactly(new UrlResource("http://localhost:8080"), + new UrlResource("http://localhost:9090")); } @Test @@ -101,7 +104,7 @@ void testGenericListElement() throws Exception { gb.setResourceList(new ArrayList<>()); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("resourceList[0]", "http://localhost:8080"); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList()).containsExactly(new UrlResource("http://localhost:8080")); } @Test @@ -198,7 +201,7 @@ void testGenericListOfLists() { BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("listOfLists[0][0]", 5); assertThat(bw.getPropertyValue("listOfLists[0][0]")).isEqualTo(5); - assertThat(gb.getListOfLists().get(0).get(0)).isEqualTo(5); + assertThat(gb.getListOfLists()).singleElement().asList().containsExactly(5); } @Test @@ -210,7 +213,7 @@ void testGenericListOfListsWithElementConversion() { BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("listOfLists[0][0]", "5"); assertThat(bw.getPropertyValue("listOfLists[0][0]")).isEqualTo(5); - assertThat(gb.getListOfLists().get(0).get(0)).isEqualTo(5); + assertThat(gb.getListOfLists()).singleElement().asList().containsExactly(5); } @Test @@ -295,7 +298,7 @@ void testGenericMapOfLists() { BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("mapOfLists[1][0]", 5); assertThat(bw.getPropertyValue("mapOfLists[1][0]")).isEqualTo(5); - assertThat(gb.getMapOfLists().get(1).get(0)).isEqualTo(5); + assertThat(gb.getMapOfLists().get(1)).containsExactly(5); } @Test @@ -307,7 +310,7 @@ void testGenericMapOfListsWithElementConversion() { BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("mapOfLists[1][0]", "5"); assertThat(bw.getPropertyValue("mapOfLists[1][0]")).isEqualTo(5); - assertThat(gb.getMapOfLists().get(1).get(0)).isEqualTo(5); + assertThat(gb.getMapOfLists().get(1)).containsExactly(5); } @Test @@ -382,8 +385,7 @@ void testComplexGenericMap() { BeanWrapper bw = new BeanWrapperImpl(holder); bw.setPropertyValue("genericMap", inputMap); - assertThat(holder.getGenericMap().keySet().iterator().next().get(0)).isEqualTo(1); - assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + assertThat(holder.getGenericMap()).containsOnly(entry(List.of(1), List.of(10L))); } @Test @@ -399,8 +401,7 @@ void testComplexGenericMapWithCollectionConversion() { BeanWrapper bw = new BeanWrapperImpl(holder); bw.setPropertyValue("genericMap", inputMap); - assertThat(holder.getGenericMap().keySet().iterator().next().get(0)).isEqualTo(1); - assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + assertThat(holder.getGenericMap()).containsOnly(entry(List.of(1), List.of(10L))); } @Test @@ -412,8 +413,7 @@ void testComplexGenericIndexedMapEntry() { BeanWrapper bw = new BeanWrapperImpl(holder); bw.setPropertyValue("genericIndexedMap[1]", inputValue); - assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + assertThat(holder.getGenericIndexedMap()).containsOnly(entry(1, List.of(10L))); } @Test @@ -425,8 +425,18 @@ void testComplexGenericIndexedMapEntryWithCollectionConversion() { BeanWrapper bw = new BeanWrapperImpl(holder); bw.setPropertyValue("genericIndexedMap[1]", inputValue); - assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + assertThat(holder.getGenericIndexedMap()).containsOnly(entry(1, List.of(10L))); + } + + @Test + void testComplexGenericIndexedMapEntryWithPlainValue() { + String inputValue = "10"; + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("genericIndexedMap[1]", inputValue); + + assertThat(holder.getGenericIndexedMap()).containsOnly(entry(1, List.of(10L))); } @Test @@ -438,8 +448,7 @@ void testComplexDerivedIndexedMapEntry() { BeanWrapper bw = new BeanWrapperImpl(holder); bw.setPropertyValue("derivedIndexedMap[1]", inputValue); - assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + assertThat(holder.getDerivedIndexedMap()).containsOnly(entry(1, List.of(10L))); } @Test @@ -451,8 +460,53 @@ void testComplexDerivedIndexedMapEntryWithCollectionConversion() { BeanWrapper bw = new BeanWrapperImpl(holder); bw.setPropertyValue("derivedIndexedMap[1]", inputValue); - assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1); - assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10)); + assertThat(holder.getDerivedIndexedMap()).containsOnly(entry(1, List.of(10L))); + } + + @Test + void testComplexDerivedIndexedMapEntryWithPlainValue() { + String inputValue = "10"; + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("derivedIndexedMap[1]", inputValue); + + assertThat(holder.getDerivedIndexedMap()).containsOnly(entry(1, List.of(10L))); + } + + @Test + void testComplexMultiValueMapEntry() { + List inputValue = new ArrayList<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("multiValueMap[1]", inputValue); + + assertThat(holder.getMultiValueMap()).containsOnly(entry(1, List.of(10L))); + } + + @Test + void testComplexMultiValueMapEntryWithCollectionConversion() { + Set inputValue = new HashSet<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("multiValueMap[1]", inputValue); + + assertThat(holder.getMultiValueMap()).containsOnly(entry(1, List.of(10L))); + } + + @Test + void testComplexMultiValueMapEntryWithPlainValue() { + String inputValue = "10"; + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("multiValueMap[1]", inputValue); + + assertThat(holder.getMultiValueMap()).containsOnly(entry(1, List.of(10L))); } @Test @@ -462,8 +516,7 @@ void testGenericallyTypedIntegerBean() { bw.setPropertyValue("genericProperty", "10"); bw.setPropertyValue("genericListProperty", new String[] {"20", "30"}); assertThat(gb.getGenericProperty()).isEqualTo(10); - assertThat(gb.getGenericListProperty().get(0)).isEqualTo(20); - assertThat(gb.getGenericListProperty().get(1)).isEqualTo(30); + assertThat(gb.getGenericListProperty()).containsExactly(20, 30); } @Test @@ -472,9 +525,9 @@ void testGenericallyTypedSetOfIntegerBean() { BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("genericProperty", "10"); bw.setPropertyValue("genericListProperty", new String[] {"20", "30"}); - assertThat(gb.getGenericProperty().iterator().next()).isEqualTo(10); - assertThat(gb.getGenericListProperty().get(0).iterator().next()).isEqualTo(20); - assertThat(gb.getGenericListProperty().get(1).iterator().next()).isEqualTo(30); + assertThat(gb.getGenericProperty()).containsExactly(10); + assertThat(gb.getGenericListProperty().get(0)).containsExactly(20); + assertThat(gb.getGenericListProperty().get(1)).containsExactly(30); } @Test @@ -518,7 +571,7 @@ public D getData() { } - private static abstract class BaseGenericCollectionBean { + private abstract static class BaseGenericCollectionBean { public abstract Object getMapOfInteger(); @@ -585,6 +638,8 @@ private static class ComplexMapHolder { private DerivedMap derivedIndexedMap = new DerivedMap(); + private MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + public void setGenericMap(Map, List> genericMap) { this.genericMap = genericMap; } @@ -608,6 +663,14 @@ public void setDerivedIndexedMap(DerivedMap derivedIndexedMap) { public DerivedMap getDerivedIndexedMap() { return derivedIndexedMap; } + + public void setMultiValueMap(MultiValueMap multiValueMap) { + this.multiValueMap = multiValueMap; + } + + public MultiValueMap getMultiValueMap() { + return multiValueMap; + } } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java index ccaf690a051a..b9c29372c123 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.beans; +import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -102,13 +103,13 @@ void setValidAndInvalidPropertyValuesShouldContainExceptionDetails() { pvs.addPropertyValue(new PropertyValue("age", "foobar")); pvs.addPropertyValue(new PropertyValue("name", newName)); pvs.addPropertyValue(new PropertyValue("touchy", invalidTouchy)); - assertThatExceptionOfType(PropertyBatchUpdateException.class).isThrownBy(() -> - accessor.setPropertyValues(pvs)) - .satisfies(ex -> { - assertThat(ex.getExceptionCount()).isEqualTo(2); - assertThat(ex.getPropertyAccessException("touchy").getPropertyChangeEvent() - .getNewValue()).isEqualTo(invalidTouchy); - }); + assertThatExceptionOfType(PropertyBatchUpdateException.class) + .isThrownBy(() -> accessor.setPropertyValues(pvs)) + .satisfies(ex -> { + assertThat(ex.getExceptionCount()).isEqualTo(2); + assertThat(ex.getPropertyAccessException("touchy").getPropertyChangeEvent() + .getNewValue()).isEqualTo(invalidTouchy); + }); // Test validly set property matches assertThat(target.getName()).as("Valid set property must stick").isEqualTo(newName); assertThat(target.getAge()).as("Invalid set property must retain old value").isEqualTo(0); @@ -118,9 +119,9 @@ void setValidAndInvalidPropertyValuesShouldContainExceptionDetails() { void checkNotWritablePropertyHoldPossibleMatches() { TestBean target = new TestBean(); BeanWrapper accessor = createAccessor(target); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - accessor.setPropertyValue("ag", "foobar")) - .satisfies(ex -> assertThat(ex.getPossibleMatches()).containsExactly("age")); + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> accessor.setPropertyValue("ag", "foobar")) + .satisfies(ex -> assertThat(ex.getPossibleMatches()).containsExactly("age")); } @Test // Can't be shared; there is no such thing as a read-only field @@ -172,10 +173,26 @@ void setPropertyTypeMismatch() { void setterOverload() { SetterOverload target = new SetterOverload(); BeanWrapper accessor = createAccessor(target); + accessor.setPropertyValue("object", "a String"); assertThat(target.value).isEqualTo("a String"); assertThat(target.getObject()).isEqualTo("a String"); assertThat(accessor.getPropertyValue("object")).isEqualTo("a String"); + + accessor.setPropertyValue("object", 1000); + assertThat(target.value).isEqualTo("1000"); + assertThat(target.getObject()).isEqualTo("1000"); + assertThat(accessor.getPropertyValue("object")).isEqualTo("1000"); + + accessor.setPropertyValue("value", 1000); + assertThat(target.value).isEqualTo("1000i"); + assertThat(target.getObject()).isEqualTo("1000i"); + assertThat(accessor.getPropertyValue("object")).isEqualTo("1000i"); + + accessor.setPropertyValue("value", Duration.ofSeconds(1000)); + assertThat(target.value).isEqualTo("1000s"); + assertThat(target.getObject()).isEqualTo("1000s"); + assertThat(accessor.getPropertyValue("object")).isEqualTo("1000s"); } @Test @@ -277,9 +294,9 @@ void getPropertyWithOptionalAndAutoGrow() { void incompletelyQuotedKeyLeadsToPropertyException() { TestBean target = new TestBean(); BeanWrapper accessor = createAccessor(target); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - accessor.setPropertyValue("[']", "foobar")) - .satisfies(ex -> assertThat(ex.getPossibleMatches()).isNull()); + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> accessor.setPropertyValue("[']", "foobar")) + .satisfies(ex -> assertThat(ex.getPossibleMatches()).isNull()); } @@ -382,7 +399,7 @@ public static class SetterOverload { public String value; public void setObject(Integer length) { - this.value = length.toString(); + this.value = length + "i"; } public void setObject(String object) { @@ -392,6 +409,14 @@ public void setObject(String object) { public String getObject() { return this.value; } + + public void setValue(int length) { + this.value = length + "i"; + } + + public void setValue(Duration duration) { + this.value = duration.getSeconds() + "s"; + } } @@ -403,7 +428,7 @@ public ActiveResource getResource() { } @Override - public void close() throws Exception { + public void close() { } } diff --git a/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java b/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java index cba7e4964a98..3e312f888bbc 100644 --- a/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * @author Chris Beams * @author Arjen Poutsma */ -public class CachedIntrospectionResultsTests { +class CachedIntrospectionResultsTests { @Test - public void acceptAndClearClassLoader() throws Exception { + void acceptAndClearClassLoader() throws Exception { BeanWrapper bw = new BeanWrapperImpl(TestBean.class); assertThat(bw.isWritableProperty("name")).isTrue(); assertThat(bw.isWritableProperty("age")).isTrue(); @@ -56,7 +56,7 @@ public void acceptAndClearClassLoader() throws Exception { } @Test - public void clearClassLoaderForSystemClassLoader() throws Exception { + void clearClassLoaderForSystemClassLoader() { BeanUtils.getPropertyDescriptors(ArrayList.class); assertThat(CachedIntrospectionResults.strongClassCache.containsKey(ArrayList.class)).isTrue(); CachedIntrospectionResults.clearClassLoader(ArrayList.class.getClassLoader()); @@ -64,7 +64,7 @@ public void clearClassLoaderForSystemClassLoader() throws Exception { } @Test - public void shouldUseExtendedBeanInfoWhenApplicable() throws NoSuchMethodException, SecurityException { + void shouldUseExtendedBeanInfoWhenApplicable() throws NoSuchMethodException, SecurityException { // given a class with a non-void returning setter method @SuppressWarnings("unused") class C { diff --git a/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java index d0d3e233270e..e7bd9109b71a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,7 +97,7 @@ private static void performSet() { // ByteArrayOutputStream does not throw // any IOException } - String value = new String(buffer.toByteArray()); + String value = buffer.toString(); BeanWrapperImpl wrapper = new BeanWrapperImpl(bean); wrapper.setPropertyValue("properties", value); diff --git a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java index 72b585bcafd0..99682ea16e00 100644 --- a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -554,11 +554,11 @@ class C { * IntrospectionException regarding a "type mismatch between indexed and non-indexed * methods" intermittently (approximately one out of every four times) under JDK 7 * due to non-deterministic results from {@link Class#getDeclaredMethods()}. - * See https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7023180 + * @see JDK-7023180 * @see #cornerSpr9702() */ @Test - void cornerSpr10111() throws Exception { + void cornerSpr10111() { assertThatNoException().isThrownBy(() -> new ExtendedBeanInfo(Introspector.getBeanInfo(BigDecimal.class))); } diff --git a/spring-beans/src/test/java/org/springframework/beans/MutablePropertyValuesTests.java b/spring-beans/src/test/java/org/springframework/beans/MutablePropertyValuesTests.java index 037933405def..489d2e0bf3c4 100644 --- a/spring-beans/src/test/java/org/springframework/beans/MutablePropertyValuesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/MutablePropertyValuesTests.java @@ -16,7 +16,9 @@ package org.springframework.beans; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -30,10 +32,10 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class MutablePropertyValuesTests extends AbstractPropertyValuesTests { +class MutablePropertyValuesTests { @Test - public void testValid() { + void valid() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValue(new PropertyValue("forname", "Tony")); pvs.addPropertyValue(new PropertyValue("surname", "Blair")); @@ -48,7 +50,7 @@ public void testValid() { } @Test - public void testAddOrOverride() { + void addOrOverride() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValue(new PropertyValue("forname", "Tony")); pvs.addPropertyValue(new PropertyValue("surname", "Blair")); @@ -56,25 +58,25 @@ public void testAddOrOverride() { doTestTony(pvs); PropertyValue addedPv = new PropertyValue("rod", "Rod"); pvs.addPropertyValue(addedPv); - assertThat(pvs.getPropertyValue("rod").equals(addedPv)).isTrue(); + assertThat(pvs.getPropertyValue("rod")).isEqualTo(addedPv); PropertyValue changedPv = new PropertyValue("forname", "Greg"); pvs.addPropertyValue(changedPv); - assertThat(pvs.getPropertyValue("forname").equals(changedPv)).isTrue(); + assertThat(pvs.getPropertyValue("forname")).isEqualTo(changedPv); } @Test - public void testChangesOnEquals() { + void changesOnEquals() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValue(new PropertyValue("forname", "Tony")); pvs.addPropertyValue(new PropertyValue("surname", "Blair")); pvs.addPropertyValue(new PropertyValue("age", "50")); MutablePropertyValues pvs2 = pvs; PropertyValues changes = pvs2.changesSince(pvs); - assertThat(changes.getPropertyValues().length).as("changes are empty").isEqualTo(0); + assertThat(changes.getPropertyValues()).as("changes are empty").isEmpty(); } @Test - public void testChangeOfOneField() { + void changeOfOneField() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValue(new PropertyValue("forname", "Tony")); pvs.addPropertyValue(new PropertyValue("surname", "Blair")); @@ -82,33 +84,31 @@ public void testChangeOfOneField() { MutablePropertyValues pvs2 = new MutablePropertyValues(pvs); PropertyValues changes = pvs2.changesSince(pvs); - assertThat(changes.getPropertyValues().length).as("changes are empty, not of length " + changes.getPropertyValues().length) - .isEqualTo(0); + assertThat(changes.getPropertyValues()).as("changes").isEmpty(); pvs2.addPropertyValue(new PropertyValue("forname", "Gordon")); changes = pvs2.changesSince(pvs); - assertThat(changes.getPropertyValues().length).as("1 change").isEqualTo(1); + assertThat(changes.getPropertyValues()).as("1 change").hasSize(1); PropertyValue fn = changes.getPropertyValue("forname"); assertThat(fn).as("change is forname").isNotNull(); - assertThat(fn.getValue().equals("Gordon")).as("new value is gordon").isTrue(); + assertThat(fn.getValue()).isEqualTo("Gordon"); MutablePropertyValues pvs3 = new MutablePropertyValues(pvs); changes = pvs3.changesSince(pvs); - assertThat(changes.getPropertyValues().length).as("changes are empty, not of length " + changes.getPropertyValues().length) - .isEqualTo(0); + assertThat(changes.getPropertyValues()).as("changes").isEmpty(); // add new pvs3.addPropertyValue(new PropertyValue("foo", "bar")); pvs3.addPropertyValue(new PropertyValue("fi", "fum")); changes = pvs3.changesSince(pvs); - assertThat(changes.getPropertyValues().length).as("2 change").isEqualTo(2); + assertThat(changes.getPropertyValues()).as("2 changes").hasSize(2); fn = changes.getPropertyValue("foo"); assertThat(fn).as("change in foo").isNotNull(); - assertThat(fn.getValue().equals("bar")).as("new value is bar").isTrue(); + assertThat(fn.getValue()).isEqualTo("bar"); } @Test - public void iteratorContainsPropertyValue() { + void iteratorContainsPropertyValue() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("foo", "bar"); @@ -122,28 +122,55 @@ public void iteratorContainsPropertyValue() { } @Test - public void iteratorIsEmptyForEmptyValues() { + void iteratorIsEmptyForEmptyValues() { MutablePropertyValues pvs = new MutablePropertyValues(); Iterator it = pvs.iterator(); assertThat(it.hasNext()).isFalse(); } @Test - public void streamContainsPropertyValue() { + void streamContainsPropertyValue() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("foo", "bar"); assertThat(pvs.stream()).isNotNull(); - assertThat(pvs.stream().count()).isEqualTo(1L); - assertThat(pvs.stream().anyMatch(pv -> "foo".equals(pv.getName()) && "bar".equals(pv.getValue()))).isTrue(); - assertThat(pvs.stream().anyMatch(pv -> "bar".equals(pv.getName()) && "foo".equals(pv.getValue()))).isFalse(); + assertThat(pvs.stream()).hasSize(1); + assertThat(pvs.stream()).anyMatch(pv -> "foo".equals(pv.getName()) && "bar".equals(pv.getValue())); + assertThat(pvs.stream()).noneMatch(pv -> "bar".equals(pv.getName()) && "foo".equals(pv.getValue())); } @Test - public void streamIsEmptyForEmptyValues() { + void streamIsEmptyForEmptyValues() { MutablePropertyValues pvs = new MutablePropertyValues(); assertThat(pvs.stream()).isNotNull(); - assertThat(pvs.stream().count()).isEqualTo(0L); + assertThat(pvs.stream()).isEmpty(); + } + + /** + * Must contain: forname=Tony surname=Blair age=50 + */ + protected void doTestTony(PropertyValues pvs) { + PropertyValue[] propertyValues = pvs.getPropertyValues(); + + assertThat(propertyValues).hasSize(3); + assertThat(pvs.contains("forname")).as("Contains forname").isTrue(); + assertThat(pvs.contains("surname")).as("Contains surname").isTrue(); + assertThat(pvs.contains("age")).as("Contains age").isTrue(); + assertThat(pvs.contains("tory")).as("Doesn't contain tory").isFalse(); + + Map map = new HashMap<>(); + map.put("forname", "Tony"); + map.put("surname", "Blair"); + map.put("age", "50"); + + for (PropertyValue element : propertyValues) { + Object val = map.get(element.getName()); + assertThat(val).as("Can't have unexpected value").isNotNull(); + assertThat(val).isInstanceOf(String.class); + assertThat(val).isEqualTo(element.getValue()); + map.remove(element.getName()); + } + assertThat(map).isEmpty(); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java index 2afa65d99d02..5332da7eae40 100644 --- a/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,22 +21,22 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link PropertyAccessorUtils}. + * Tests for {@link PropertyAccessorUtils}. * * @author Juergen Hoeller * @author Chris Beams */ -public class PropertyAccessorUtilsTests { +class PropertyAccessorUtilsTests { @Test - public void getPropertyName() { + void getPropertyName() { assertThat(PropertyAccessorUtils.getPropertyName("")).isEmpty(); assertThat(PropertyAccessorUtils.getPropertyName("[user]")).isEmpty(); assertThat(PropertyAccessorUtils.getPropertyName("user")).isEqualTo("user"); } @Test - public void isNestedOrIndexedProperty() { + void isNestedOrIndexedProperty() { assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty(null)).isFalse(); assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty("")).isFalse(); assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty("user")).isFalse(); @@ -46,19 +46,19 @@ public void isNestedOrIndexedProperty() { } @Test - public void getFirstNestedPropertySeparatorIndex() { + void getFirstNestedPropertySeparatorIndex() { assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("[user]")).isEqualTo(-1); assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("user.name")).isEqualTo(4); } @Test - public void getLastNestedPropertySeparatorIndex() { + void getLastNestedPropertySeparatorIndex() { assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("[user]")).isEqualTo(-1); assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("user.address.street")).isEqualTo(12); } @Test - public void matchesProperty() { + void matchesProperty() { assertThat(PropertyAccessorUtils.matchesProperty("user", "email")).isFalse(); assertThat(PropertyAccessorUtils.matchesProperty("username", "user")).isFalse(); assertThat(PropertyAccessorUtils.matchesProperty("admin[user]", "user")).isFalse(); @@ -68,7 +68,7 @@ public void matchesProperty() { } @Test - public void canonicalPropertyName() { + void canonicalPropertyName() { assertThat(PropertyAccessorUtils.canonicalPropertyName(null)).isEmpty(); assertThat(PropertyAccessorUtils.canonicalPropertyName("map")).isEqualTo("map"); assertThat(PropertyAccessorUtils.canonicalPropertyName("map[key1]")).isEqualTo("map[key1]"); @@ -82,7 +82,7 @@ public void canonicalPropertyName() { } @Test - public void canonicalPropertyNames() { + void canonicalPropertyNames() { assertThat(PropertyAccessorUtils.canonicalPropertyNames(null)).isNull(); String[] original = diff --git a/spring-beans/src/test/java/org/springframework/beans/PropertyMatchesTests.java b/spring-beans/src/test/java/org/springframework/beans/PropertyMatchesTests.java index 09e72571e00f..5dc2b0f42aff 100644 --- a/spring-beans/src/test/java/org/springframework/beans/PropertyMatchesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/PropertyMatchesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,28 +30,28 @@ * * @author Stephane Nicoll */ -public class PropertyMatchesTests { +class PropertyMatchesTests { @Test - public void simpleBeanPropertyTypo() { + void simpleBeanPropertyTypo() { PropertyMatches matches = PropertyMatches.forProperty("naem", SampleBeanProperties.class); assertThat(matches.getPossibleMatches()).contains("name"); } @Test - public void complexBeanPropertyTypo() { + void complexBeanPropertyTypo() { PropertyMatches matches = PropertyMatches.forProperty("desriptn", SampleBeanProperties.class); assertThat(matches.getPossibleMatches()).isEmpty(); } @Test - public void unknownBeanProperty() { + void unknownBeanProperty() { PropertyMatches matches = PropertyMatches.forProperty("unknown", SampleBeanProperties.class); assertThat(matches.getPossibleMatches()).isEmpty(); } @Test - public void severalMatchesBeanProperty() { + void severalMatchesBeanProperty() { PropertyMatches matches = PropertyMatches.forProperty("counter", SampleBeanProperties.class); assertThat(matches.getPossibleMatches()).contains("counter1"); assertThat(matches.getPossibleMatches()).contains("counter2"); @@ -59,7 +59,7 @@ public void severalMatchesBeanProperty() { } @Test - public void simpleBeanPropertyErrorMessage() { + void simpleBeanPropertyErrorMessage() { PropertyMatches matches = PropertyMatches.forProperty("naem", SampleBeanProperties.class); String msg = matches.buildErrorMessage(); assertThat(msg).contains("naem"); @@ -69,7 +69,7 @@ public void simpleBeanPropertyErrorMessage() { } @Test - public void complexBeanPropertyErrorMessage() { + void complexBeanPropertyErrorMessage() { PropertyMatches matches = PropertyMatches.forProperty("counter", SampleBeanProperties.class); String msg = matches.buildErrorMessage(); assertThat(msg).contains("counter"); @@ -79,25 +79,25 @@ public void complexBeanPropertyErrorMessage() { } @Test - public void simpleFieldPropertyTypo() { + void simpleFieldPropertyTypo() { PropertyMatches matches = PropertyMatches.forField("naem", SampleFieldProperties.class); assertThat(matches.getPossibleMatches()).contains("name"); } @Test - public void complexFieldPropertyTypo() { + void complexFieldPropertyTypo() { PropertyMatches matches = PropertyMatches.forField("desriptn", SampleFieldProperties.class); assertThat(matches.getPossibleMatches()).isEmpty(); } @Test - public void unknownFieldProperty() { + void unknownFieldProperty() { PropertyMatches matches = PropertyMatches.forField("unknown", SampleFieldProperties.class); assertThat(matches.getPossibleMatches()).isEmpty(); } @Test - public void severalMatchesFieldProperty() { + void severalMatchesFieldProperty() { PropertyMatches matches = PropertyMatches.forField("counter", SampleFieldProperties.class); assertThat(matches.getPossibleMatches()).contains("counter1"); assertThat(matches.getPossibleMatches()).contains("counter2"); @@ -105,7 +105,7 @@ public void severalMatchesFieldProperty() { } @Test - public void simpleFieldPropertyErrorMessage() { + void simpleFieldPropertyErrorMessage() { PropertyMatches matches = PropertyMatches.forField("naem", SampleFieldProperties.class); String msg = matches.buildErrorMessage(); assertThat(msg).contains("naem"); @@ -115,7 +115,7 @@ public void simpleFieldPropertyErrorMessage() { } @Test - public void complexFieldPropertyErrorMessage() { + void complexFieldPropertyErrorMessage() { PropertyMatches matches = PropertyMatches.forField("counter", SampleFieldProperties.class); String msg = matches.buildErrorMessage(); assertThat(msg).contains("counter"); diff --git a/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java b/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java index 4a154ef10a47..d4243094b59f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ * @author Chris Beams * @see ExtendedBeanInfoTests */ -public class SimplePropertyDescriptorTests { +class SimplePropertyDescriptorTests { @Test - public void toStringOutput() throws IntrospectionException, SecurityException, NoSuchMethodException { + void toStringOutput() throws IntrospectionException, SecurityException, NoSuchMethodException { { Object pd = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", null, null); assertThat(pd.toString()).contains( @@ -71,7 +71,7 @@ class C { } @Test - public void nonIndexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { + void nonIndexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { Object pd1 = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", null, null); assertThat(pd1).isEqualTo(pd1); @@ -108,7 +108,7 @@ class C { } @Test - public void indexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { + void indexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { Object pd1 = new ExtendedBeanInfo.SimpleIndexedPropertyDescriptor("foo", null, null, null, null); assertThat(pd1).isEqualTo(pd1); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java index 5ba2aec57cc2..e60d9d6d65a8 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ * @author Sam Brannen * @since 04.07.2003 */ -public class BeanFactoryUtilsTests { +class BeanFactoryUtilsTests { private static final Class CLASS = BeanFactoryUtilsTests.class; private static final Resource ROOT_CONTEXT = qualifiedResource(CLASS, "root.xml"); @@ -63,7 +63,7 @@ public class BeanFactoryUtilsTests { @BeforeEach - public void setup() { + void setup() { // Interesting hierarchical factory to test counts. DefaultListableBeanFactory grandParent = new DefaultListableBeanFactory(); @@ -81,7 +81,7 @@ public void setup() { @Test - public void testHierarchicalCountBeansWithNonHierarchicalFactory() { + void testHierarchicalCountBeansWithNonHierarchicalFactory() { StaticListableBeanFactory lbf = new StaticListableBeanFactory(); lbf.addBean("t1", new TestBean()); lbf.addBean("t2", new TestBean()); @@ -92,7 +92,7 @@ public void testHierarchicalCountBeansWithNonHierarchicalFactory() { * Check that override doesn't count as two separate beans. */ @Test - public void testHierarchicalCountBeansWithOverride() { + void testHierarchicalCountBeansWithOverride() { // Leaf count assertThat(this.listableBeanFactory.getBeanDefinitionCount()).isEqualTo(1); // Count minus duplicate @@ -101,44 +101,44 @@ public void testHierarchicalCountBeansWithOverride() { } @Test - public void testHierarchicalNamesWithNoMatch() { + void testHierarchicalNamesWithNoMatch() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, NoOp.class)); assertThat(names).isEmpty(); } @Test - public void testHierarchicalNamesWithMatchOnlyInRoot() { + void testHierarchicalNamesWithMatchOnlyInRoot() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, IndexedTestBean.class)); assertThat(names).hasSize(1); - assertThat(names.contains("indexedBean")).isTrue(); + assertThat(names).contains("indexedBean"); // Distinguish from default ListableBeanFactory behavior assertThat(listableBeanFactory.getBeanNamesForType(IndexedTestBean.class)).isEmpty(); } @Test - public void testGetBeanNamesForTypeWithOverride() { + void testGetBeanNamesForTypeWithOverride() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class)); // includes 2 TestBeans from FactoryBeans (DummyFactory definitions) assertThat(names).hasSize(4); - assertThat(names.contains("test")).isTrue(); - assertThat(names.contains("test3")).isTrue(); - assertThat(names.contains("testFactory1")).isTrue(); - assertThat(names.contains("testFactory2")).isTrue(); + assertThat(names).contains("test"); + assertThat(names).contains("test3"); + assertThat(names).contains("testFactory1"); + assertThat(names).contains("testFactory2"); } @Test - public void testNoBeansOfType() { + void testNoBeansOfType() { StaticListableBeanFactory lbf = new StaticListableBeanFactory(); lbf.addBean("foo", new Object()); Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, ITestBean.class, true, false); - assertThat(beans.isEmpty()).isTrue(); + assertThat(beans).isEmpty(); } @Test - public void testFindsBeansOfTypeWithStaticFactory() { + void testFindsBeansOfTypeWithStaticFactory() { StaticListableBeanFactory lbf = new StaticListableBeanFactory(); TestBean t1 = new TestBean(); TestBean t2 = new TestBean(); @@ -155,7 +155,7 @@ public void testFindsBeansOfTypeWithStaticFactory() { assertThat(beans.get("t1")).isEqualTo(t1); assertThat(beans.get("t2")).isEqualTo(t2); assertThat(beans.get("t3")).isEqualTo(t3.getObject()); - assertThat(beans.get("t4") instanceof TestBean).isTrue(); + assertThat(beans.get("t4")).isInstanceOf(TestBean.class); beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, DummyFactory.class, true, true); assertThat(beans).hasSize(2); @@ -169,7 +169,7 @@ public void testFindsBeansOfTypeWithStaticFactory() { } @Test - public void testFindsBeansOfTypeWithDefaultFactory() { + void testFindsBeansOfTypeWithDefaultFactory() { Object test3 = this.listableBeanFactory.getBean("test3"); Object test = this.listableBeanFactory.getBean("test"); @@ -191,7 +191,7 @@ public void testFindsBeansOfTypeWithDefaultFactory() { assertThat(beans.get("t1")).isEqualTo(t1); assertThat(beans.get("t2")).isEqualTo(t2); assertThat(beans.get("t3")).isEqualTo(t3.getObject()); - assertThat(beans.get("t4") instanceof TestBean).isTrue(); + assertThat(beans.get("t4")).isInstanceOf(TestBean.class); // t3 and t4 are found here as of Spring 2.0, since they are pre-registered // singleton instances, while testFactory1 and testFactory are *not* found // because they are FactoryBean definitions that haven't been initialized yet. @@ -210,11 +210,11 @@ public void testFindsBeansOfTypeWithDefaultFactory() { assertThat(beans.get("test3")).isEqualTo(test3); assertThat(beans.get("test")).isEqualTo(test); assertThat(beans.get("testFactory1")).isEqualTo(testFactory1); - assertThat(beans.get("testFactory2") instanceof TestBean).isTrue(); + assertThat(beans.get("testFactory2")).isInstanceOf(TestBean.class); assertThat(beans.get("t1")).isEqualTo(t1); assertThat(beans.get("t2")).isEqualTo(t2); assertThat(beans.get("t3")).isEqualTo(t3.getObject()); - assertThat(beans.get("t4") instanceof TestBean).isTrue(); + assertThat(beans.get("t4")).isInstanceOf(TestBean.class); beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, DummyFactory.class, true, true); assertThat(beans).hasSize(4); @@ -232,7 +232,7 @@ public void testFindsBeansOfTypeWithDefaultFactory() { } @Test - public void testHierarchicalResolutionWithOverride() { + void testHierarchicalResolutionWithOverride() { Object test3 = this.listableBeanFactory.getBean("test3"); Object test = this.listableBeanFactory.getBean("test"); @@ -257,7 +257,7 @@ public void testHierarchicalResolutionWithOverride() { assertThat(beans.get("test3")).isEqualTo(test3); assertThat(beans.get("test")).isEqualTo(test); assertThat(beans.get("testFactory1")).isEqualTo(testFactory1); - assertThat(beans.get("testFactory2") instanceof TestBean).isTrue(); + assertThat(beans.get("testFactory2")).isInstanceOf(TestBean.class); beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, DummyFactory.class, true, true); assertThat(beans).hasSize(2); @@ -271,59 +271,59 @@ public void testHierarchicalResolutionWithOverride() { } @Test - public void testHierarchicalNamesForAnnotationWithNoMatch() { + void testHierarchicalNamesForAnnotationWithNoMatch() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, Override.class)); assertThat(names).isEmpty(); } @Test - public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() { + void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, TestAnnotation.class)); assertThat(names).hasSize(1); - assertThat(names.contains("annotatedBean")).isTrue(); + assertThat(names).contains("annotatedBean"); // Distinguish from default ListableBeanFactory behavior assertThat(listableBeanFactory.getBeanNamesForAnnotation(TestAnnotation.class)).isEmpty(); } @Test - public void testGetBeanNamesForAnnotationWithOverride() { + void testGetBeanNamesForAnnotationWithOverride() { AnnotatedBean annotatedBean = new AnnotatedBean(); this.listableBeanFactory.registerSingleton("anotherAnnotatedBean", annotatedBean); List names = Arrays.asList( BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, TestAnnotation.class)); assertThat(names).hasSize(2); - assertThat(names.contains("annotatedBean")).isTrue(); - assertThat(names.contains("anotherAnnotatedBean")).isTrue(); + assertThat(names).contains("annotatedBean"); + assertThat(names).contains("anotherAnnotatedBean"); } @Test - public void testADependencies() { + void testADependencies() { String[] deps = this.dependentBeansFactory.getDependentBeans("a"); assertThat(ObjectUtils.isEmpty(deps)).isTrue(); } @Test - public void testBDependencies() { + void testBDependencies() { String[] deps = this.dependentBeansFactory.getDependentBeans("b"); assertThat(Arrays.equals(new String[] { "c" }, deps)).isTrue(); } @Test - public void testCDependencies() { + void testCDependencies() { String[] deps = this.dependentBeansFactory.getDependentBeans("c"); assertThat(Arrays.equals(new String[] { "int", "long" }, deps)).isTrue(); } @Test - public void testIntDependencies() { + void testIntDependencies() { String[] deps = this.dependentBeansFactory.getDependentBeans("int"); assertThat(Arrays.equals(new String[] { "buffer" }, deps)).isTrue(); } @Test - public void findAnnotationOnBean() { + void findAnnotationOnBean() { this.listableBeanFactory.registerSingleton("controllerAdvice", new ControllerAdviceClass()); this.listableBeanFactory.registerSingleton("restControllerAdvice", new RestControllerAdviceClass()); testFindAnnotationOnBean(this.listableBeanFactory); @@ -350,7 +350,7 @@ private void assertControllerAdvice(ListableBeanFactory lbf, String beanName) { } @Test - public void isSingletonAndIsPrototypeWithStaticFactory() { + void isSingletonAndIsPrototypeWithStaticFactory() { StaticListableBeanFactory lbf = new StaticListableBeanFactory(); TestBean bean = new TestBean(); DummyFactory fb1 = new DummyFactory(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index 98f69c44d402..3f350cdbb0ec 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.net.MalformedURLException; import java.text.NumberFormat; import java.text.ParseException; +import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; @@ -65,6 +66,7 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.beans.factory.support.ChildBeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ConstructorDependenciesBean; @@ -78,9 +80,11 @@ import org.springframework.beans.testfixture.beans.factory.DummyFactory; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.io.Resource; @@ -652,9 +656,7 @@ void arrayReferenceByAutowire() { lbf.registerSingleton("string2", "B"); TestBean self = (TestBean) lbf.getBean("self"); - assertThat(self.getStringArray()).hasSize(2); - assertThat(self.getStringArray()).contains("A"); - assertThat(self.getStringArray()).contains("B"); + assertThat(self.getStringArray()).containsOnly("A","B"); } @Test @@ -665,13 +667,13 @@ void possibleMatches() { bd.setPropertyValues(pvs); lbf.registerBeanDefinition("tb", bd); - assertThatExceptionOfType(BeanCreationException.class).as("invalid property").isThrownBy(() -> - lbf.getBean("tb")) - .withCauseInstanceOf(NotWritablePropertyException.class) - .satisfies(ex -> { - NotWritablePropertyException cause = (NotWritablePropertyException) ex.getCause(); - assertThat(cause.getPossibleMatches()).containsExactly("age"); - }); + assertThatExceptionOfType(BeanCreationException.class).as("invalid property") + .isThrownBy(() -> lbf.getBean("tb")) + .withCauseInstanceOf(NotWritablePropertyException.class) + .satisfies(ex -> { + NotWritablePropertyException cause = (NotWritablePropertyException) ex.getCause(); + assertThat(cause.getPossibleMatches()).containsExactly("age"); + }); } @Test @@ -719,9 +721,9 @@ void prototypeCircleLeadsToException() { p.setProperty("rod.spouse", "*kerry"); registerBeanDefinitions(p); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.getBean("kerry")) - .satisfies(ex -> assertThat(ex.contains(BeanCurrentlyInCreationException.class)).isTrue()); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.getBean("kerry")) + .satisfies(ex -> assertThat(ex.contains(BeanCurrentlyInCreationException.class)).isTrue()); } @Test @@ -902,16 +904,16 @@ void beanDefinitionOverridingNotAllowed() { lbf.registerBeanDefinition("test", oldDef); lbf.registerAlias("test", "testX"); - assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() -> - lbf.registerBeanDefinition("test", newDef)) + assertThatExceptionOfType(BeanDefinitionOverrideException.class) + .isThrownBy(() -> lbf.registerBeanDefinition("test", newDef)) .satisfies(ex -> { assertThat(ex.getBeanName()).isEqualTo("test"); assertThat(ex.getBeanDefinition()).isEqualTo(newDef); assertThat(ex.getExistingDefinition()).isEqualTo(oldDef); }); - assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() -> - lbf.registerBeanDefinition("testX", newDef)) + assertThatExceptionOfType(BeanDefinitionOverrideException.class) + .isThrownBy(() -> lbf.registerBeanDefinition("testX", newDef)) .satisfies(ex -> { assertThat(ex.getBeanName()).isEqualTo("testX"); assertThat(ex.getBeanDefinition()).isEqualTo(newDef); @@ -1160,7 +1162,7 @@ void registerExistingSingletonWithNameOverriding() { assertThat(lbf.getBean("singletonObject")).isEqualTo(singletonObject); assertThat(test.getSpouse()).isEqualTo(singletonObject); - Map beansOfType = lbf.getBeansOfType(TestBean.class, false, true); + Map beansOfType = lbf.getBeansOfType(TestBean.class, false, true); assertThat(beansOfType).hasSize(2); assertThat(beansOfType.containsValue(test)).isTrue(); assertThat(beansOfType.containsValue(singletonObject)).isTrue(); @@ -1237,7 +1239,7 @@ void arrayPropertyWithAutowiring() throws MalformedURLException { } @Test - void arrayPropertyWithOptionalAutowiring() throws MalformedURLException { + void arrayPropertyWithOptionalAutowiring() { RootBeanDefinition rbd = new RootBeanDefinition(ArrayBean.class); rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); lbf.registerBeanDefinition("arrayBean", rbd); @@ -1319,6 +1321,34 @@ void expressionInStringArray() { assertThat(properties.getProperty("foo")).isEqualTo("bar"); } + @Test + void withOverloadedSetters() { + RootBeanDefinition rbd = new RootBeanDefinition(SetterOverload.class); + rbd.getPropertyValues().add("object", "a String"); + lbf.registerBeanDefinition("overloaded", rbd); + assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("a String"); + + rbd = new RootBeanDefinition(SetterOverload.class); + rbd.getPropertyValues().add("object", 1000); + lbf.registerBeanDefinition("overloaded", rbd); + assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("1000"); + + rbd = new RootBeanDefinition(SetterOverload.class); + rbd.getPropertyValues().add("value", 1000); + lbf.registerBeanDefinition("overloaded", rbd); + assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("1000i"); + + rbd = new RootBeanDefinition(SetterOverload.class); + rbd.getPropertyValues().add("value", Duration.ofSeconds(1000)); + lbf.registerBeanDefinition("overloaded", rbd); + assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("1000s"); + + rbd = new RootBeanDefinition(SetterOverload.class); + rbd.getPropertyValues().add("value", "1000"); + lbf.registerBeanDefinition("overloaded", rbd); + assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("1000i"); + } + @Test void autowireWithNoDependencies() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); @@ -1432,6 +1462,106 @@ void autowireBeanByNameWithNoDependencyCheck() { assertThat(bean.getSpouse()).isNull(); } + @Test + void autowirePreferredConstructors() { + lbf.registerBeanDefinition("spouse1", new RootBeanDefinition(TestBean.class)); + lbf.registerBeanDefinition("spouse2", new RootBeanDefinition(TestBean.class)); + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependenciesBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("bean", bd); + lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); + + ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); + Object spouse1 = lbf.getBean("spouse1"); + Object spouse2 = lbf.getBean("spouse2"); + assertThat(bean.getSpouse1()).isSameAs(spouse1); + assertThat(bean.getSpouse2()).isSameAs(spouse2); + } + + @Test + void autowirePreferredConstructorsFromAttribute() { + lbf.registerBeanDefinition("spouse1", new RootBeanDefinition(TestBean.class)); + lbf.registerBeanDefinition("spouse2", new RootBeanDefinition(TestBean.class)); + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(ConstructorDependenciesBean.class); + bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, + ConstructorDependenciesBean.class.getConstructors()); + lbf.registerBeanDefinition("bean", bd); + lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); + + ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); + Object spouse1 = lbf.getBean("spouse1"); + Object spouse2 = lbf.getBean("spouse2"); + assertThat(bean.getSpouse1()).isSameAs(spouse1); + assertThat(bean.getSpouse2()).isSameAs(spouse2); + } + + @Test + void autowirePreferredConstructorFromAttribute() throws Exception { + lbf.registerBeanDefinition("spouse1", new RootBeanDefinition(TestBean.class)); + lbf.registerBeanDefinition("spouse2", new RootBeanDefinition(TestBean.class)); + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(ConstructorDependenciesBean.class); + bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, + ConstructorDependenciesBean.class.getConstructor(TestBean.class)); + lbf.registerBeanDefinition("bean", bd); + lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); + + ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); + Object spouse = lbf.getBean("spouse1"); + assertThat(bean.getSpouse1()).isSameAs(spouse); + assertThat(bean.getSpouse2()).isNull(); + } + + @Test + void orderFromAttribute() { + GenericBeanDefinition bd1 = new GenericBeanDefinition(); + bd1.setBeanClass(TestBean.class); + bd1.setPropertyValues(new MutablePropertyValues(List.of(new PropertyValue("name", "lowest")))); + bd1.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.LOWEST_PRECEDENCE); + lbf.registerBeanDefinition("bean1", bd1); + GenericBeanDefinition bd2 = new GenericBeanDefinition(); + bd2.setBeanClass(TestBean.class); + bd2.setPropertyValues(new MutablePropertyValues(List.of(new PropertyValue("name", "highest")))); + bd2.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.HIGHEST_PRECEDENCE); + lbf.registerBeanDefinition("bean2", bd2); + assertThat(lbf.getBeanProvider(TestBean.class).orderedStream().map(TestBean::getName)) + .containsExactly("highest", "lowest"); + } + + @Test + void orderFromAttributeOverrideAnnotation() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition rbd1 = new RootBeanDefinition(LowestPrecedenceTestBeanFactoryBean.class); + rbd1.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.HIGHEST_PRECEDENCE); + lbf.registerBeanDefinition("lowestPrecedenceFactory", rbd1); + RootBeanDefinition rbd2 = new RootBeanDefinition(HighestPrecedenceTestBeanFactoryBean.class); + rbd2.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.LOWEST_PRECEDENCE); + lbf.registerBeanDefinition("highestPrecedenceFactory", rbd2); + GenericBeanDefinition bd1 = new GenericBeanDefinition(); + bd1.setFactoryBeanName("highestPrecedenceFactory"); + lbf.registerBeanDefinition("bean1", bd1); + GenericBeanDefinition bd2 = new GenericBeanDefinition(); + bd2.setFactoryBeanName("lowestPrecedenceFactory"); + lbf.registerBeanDefinition("bean2", bd2); + assertThat(lbf.getBeanProvider(TestBean.class).orderedStream().map(TestBean::getName)) + .containsExactly("fromLowestPrecedenceTestBeanFactoryBean", "fromHighestPrecedenceTestBeanFactoryBean"); + } + + @Test + void invalidOrderAttribute() { + GenericBeanDefinition bd1 = new GenericBeanDefinition(); + bd1.setBeanClass(TestBean.class); + bd1.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Boolean.TRUE); + lbf.registerBeanDefinition("bean1", bd1); + GenericBeanDefinition bd2 = new GenericBeanDefinition(); + bd2.setBeanClass(TestBean.class); + lbf.registerBeanDefinition("bean", bd2); + assertThatIllegalStateException() + .isThrownBy(() -> lbf.getBeanProvider(TestBean.class).orderedStream().collect(Collectors.toList())) + .withMessageContaining("Invalid value type for attribute"); + } + @Test void dependsOnCycle() { RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); @@ -1562,9 +1692,9 @@ void getBeanByTypeWithMultiplePrimary() { lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(TestBean.class)) - .withMessageContaining("more than one 'primary'"); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(TestBean.class)) + .withMessageContaining("more than one 'primary'"); } @Test @@ -1742,19 +1872,19 @@ void getBeanByTypeInstanceWithAmbiguity() { resolved.add(instance); } assertThat(resolved).hasSize(2); - assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); - assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + assertThat(resolved).contains(lbf.getBean("bd1")); + assertThat(resolved).contains(lbf.getBean("bd2")); resolved = new HashSet<>(); provider.forEach(resolved::add); assertThat(resolved).hasSize(2); - assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); - assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + assertThat(resolved).contains(lbf.getBean("bd1")); + assertThat(resolved).contains(lbf.getBean("bd2")); resolved = provider.stream().collect(Collectors.toSet()); assertThat(resolved).hasSize(2); - assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); - assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + assertThat(resolved).contains(lbf.getBean("bd1")); + assertThat(resolved).contains(lbf.getBean("bd2")); } @Test @@ -1791,19 +1921,19 @@ void getBeanByTypeInstanceWithPrimary() { resolved.add(instance); } assertThat(resolved).hasSize(2); - assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); - assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + assertThat(resolved).contains(lbf.getBean("bd1")); + assertThat(resolved).contains(lbf.getBean("bd2")); resolved = new HashSet<>(); provider.forEach(resolved::add); assertThat(resolved).hasSize(2); - assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); - assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + assertThat(resolved).contains(lbf.getBean("bd1")); + assertThat(resolved).contains(lbf.getBean("bd2")); resolved = provider.stream().collect(Collectors.toSet()); assertThat(resolved).hasSize(2); - assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); - assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + assertThat(resolved).contains(lbf.getBean("bd1")); + assertThat(resolved).contains(lbf.getBean("bd2")); } @Test @@ -1837,6 +1967,42 @@ void getBeanByTypeInstanceFiltersOutNonAutowireCandidates() { lbf.getBean(TestBean.class, 67)); } + @Test + void getBeanByTypeInstanceWithConstructorIgnoresInstanceSupplier() { + RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99); + bd1.setInstanceSupplier(() -> new ConstructorDependency(new TestBean("test"))); + lbf.registerBeanDefinition("bd1", bd1); + + ConstructorDependency defaultInstance = lbf.getBean(ConstructorDependency.class); + assertThat(defaultInstance.beanName).isEqualTo("bd1"); + assertThat(defaultInstance.spouseAge).isEqualTo(0); + + ConstructorDependency argsInstance = lbf.getBean(ConstructorDependency.class, 42); + assertThat(argsInstance.beanName).isEqualTo("bd1"); + assertThat(argsInstance.spouseAge).isEqualTo(42); + } + + @Test + void getBeanByTypeInstanceWithFactoryMethodIgnoresInstanceSupplier() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd1.setFactoryBeanName("config"); + bd1.setFactoryMethodName("create"); + bd1.setInstanceSupplier(() -> new TestBean("test")); + lbf.registerBeanDefinition("config", new RootBeanDefinition(BeanWithFactoryMethod.class)); + lbf.registerBeanDefinition("bd1", bd1); + + TestBean defaultInstance = lbf.getBean(TestBean.class); + assertThat(defaultInstance.getBeanName()).isEqualTo("bd1"); + assertThat(defaultInstance.getName()).isEqualTo("test"); + assertThat(defaultInstance.getAge()).isEqualTo(0); + + TestBean argsInstance = lbf.getBean(TestBean.class, "another", 42); + assertThat(argsInstance.getBeanName()).isEqualTo("bd1"); + assertThat(argsInstance.getName()).isEqualTo("another"); + assertThat(argsInstance.getAge()).isEqualTo(42); + } + @Test @SuppressWarnings("rawtypes") void beanProviderSerialization() throws Exception { @@ -2102,7 +2268,7 @@ void autowireBeanByTypePrimaryTakesPrecedenceOverPriority() { } @Test - void beanProviderWithParentBeanFactoryReuseOrder() { + void beanProviderWithParentBeanFactoryDetectsOrder() { DefaultListableBeanFactory parentBf = new DefaultListableBeanFactory(); parentBf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); parentBf.registerBeanDefinition("regular", new RootBeanDefinition(TestBean.class)); @@ -2110,10 +2276,36 @@ void beanProviderWithParentBeanFactoryReuseOrder() { lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); lbf.setParentBeanFactory(parentBf); lbf.registerBeanDefinition("low", new RootBeanDefinition(LowPriorityTestBean.class)); + Stream> orderedTypes = lbf.getBeanProvider(TestBean.class).orderedStream().map(Object::getClass); assertThat(orderedTypes).containsExactly(HighPriorityTestBean.class, LowPriorityTestBean.class, TestBean.class); } + @Test // gh-28374 + void beanProviderWithParentBeanFactoryAndMixedOrder() { + DefaultListableBeanFactory parentBf = new DefaultListableBeanFactory(); + parentBf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + lbf.setParentBeanFactory(parentBf); + + lbf.registerSingleton("plainTestBean", new TestBean()); + + RootBeanDefinition bd1 = new RootBeanDefinition(PriorityTestBeanFactory.class); + bd1.setFactoryMethodName("lowPriorityTestBean"); + lbf.registerBeanDefinition("lowPriorityTestBean", bd1); + + RootBeanDefinition bd2 = new RootBeanDefinition(PriorityTestBeanFactory.class); + bd2.setFactoryMethodName("highPriorityTestBean"); + parentBf.registerBeanDefinition("highPriorityTestBean", bd2); + + ObjectProvider testBeanProvider = lbf.getBeanProvider(ResolvableType.forClass(TestBean.class)); + List resolved = testBeanProvider.orderedStream().toList(); + assertThat(resolved).containsExactly( + lbf.getBean("highPriorityTestBean", TestBean.class), + lbf.getBean("lowPriorityTestBean", TestBean.class), + lbf.getBean("plainTestBean", TestBean.class)); + } + @Test void autowireExistingBeanByName() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); @@ -2978,7 +3170,7 @@ public void close() { } - public static abstract class BaseClassWithDestroyMethod { + public abstract static class BaseClassWithDestroyMethod { public abstract BaseClassWithDestroyMethod close(); } @@ -3017,6 +3209,10 @@ public TestBean create() { return tb; } + public TestBean create(String name, int age) { + return new TestBean(name, age); + } + public TestBean createWithArgs(String arg) { TestBean tb = new TestBean(); tb.setName(arg); @@ -3037,7 +3233,7 @@ public interface RepositoryFactoryInformation { } - public static abstract class RepositoryFactoryBeanSupport, S, ID extends Serializable> + public abstract static class RepositoryFactoryBeanSupport, S, ID extends Serializable> implements RepositoryFactoryInformation, FactoryBean { } @@ -3082,7 +3278,7 @@ public static class TestRepositoryFactoryBean, S, ID extends RepositoryFactoryBeanSupport { @Override - public T getObject() throws Exception { + public T getObject() { throw new IllegalArgumentException("Should not be called"); } @@ -3092,6 +3288,7 @@ public Class getObjectType() { } } + public record City(String name) {} public static class CityRepository implements Repository {} @@ -3201,6 +3398,32 @@ public Resource[] getResourceArray() { } + public static class SetterOverload { + + public String value; + + public void setObject(Integer length) { + this.value = length + "i"; + } + + public void setObject(String object) { + this.value = object; + } + + public String getObject() { + return this.value; + } + + public void setValue(Duration duration) { + this.value = duration.getSeconds() + "s"; + } + + public void setValue(int length) { + this.value = length + "i"; + } + } + + /** * Bean with a dependency on a {@link FactoryBean}. */ @@ -3287,6 +3510,18 @@ private static class LowPriorityTestBean extends TestBean { } + static class PriorityTestBeanFactory { + + public static LowPriorityTestBean lowPriorityTestBean() { + return new LowPriorityTestBean(); + } + + public static HighPriorityTestBean highPriorityTestBean() { + return new HighPriorityTestBean(); + } + } + + private static class NullTestBeanFactoryBean implements FactoryBean { @Override @@ -3319,7 +3554,7 @@ public TestBeanRecipient(TestBean testBean) { enum NonPublicEnum { - VALUE_1, VALUE_2; + VALUE_1, VALUE_2 } @@ -3336,4 +3571,34 @@ public NonPublicEnum getNonPublicEnum() { } } + + @Order + private static class LowestPrecedenceTestBeanFactoryBean implements FactoryBean { + + @Override + public TestBean getObject() { + return new TestBean("fromLowestPrecedenceTestBeanFactoryBean"); + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + } + + + @Order(Ordered.HIGHEST_PRECEDENCE) + private static class HighestPrecedenceTestBeanFactoryBean implements FactoryBean { + + @Override + public TestBean getObject() { + return new TestBean("fromHighestPrecedenceTestBeanFactoryBean"); + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java index 9075bb9b37c4..a8546f37481b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,42 +32,42 @@ * * @author Chris Beams */ -public class FactoryBeanLookupTests { +class FactoryBeanLookupTests { private BeanFactory beanFactory; @BeforeEach - public void setUp() { + void setUp() { beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory).loadBeanDefinitions( new ClassPathResource("FactoryBeanLookupTests-context.xml", this.getClass())); } @Test - public void factoryBeanLookupByNameDereferencing() { + void factoryBeanLookupByNameDereferencing() { Object fooFactory = beanFactory.getBean("&fooFactory"); assertThat(fooFactory).isInstanceOf(FooFactoryBean.class); } @Test - public void factoryBeanLookupByType() { + void factoryBeanLookupByType() { FooFactoryBean fooFactory = beanFactory.getBean(FooFactoryBean.class); assertThat(fooFactory).isNotNull(); } @Test - public void factoryBeanLookupByTypeAndNameDereference() { + void factoryBeanLookupByTypeAndNameDereference() { FooFactoryBean fooFactory = beanFactory.getBean("&fooFactory", FooFactoryBean.class); assertThat(fooFactory).isNotNull(); } @Test - public void factoryBeanObjectLookupByName() { + void factoryBeanObjectLookupByName() { Object fooFactory = beanFactory.getBean("fooFactory"); assertThat(fooFactory).isInstanceOf(Foo.class); } @Test - public void factoryBeanObjectLookupByNameAndType() { + void factoryBeanObjectLookupByNameAndType() { Foo foo = beanFactory.getBean("fooFactory", Foo.class); assertThat(foo).isNotNull(); } @@ -75,7 +75,7 @@ public void factoryBeanObjectLookupByNameAndType() { class FooFactoryBean extends AbstractFactoryBean { @Override - protected Foo createInstance() throws Exception { + protected Foo createInstance() { return new Foo(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanTests.java index 9a6f86a8382c..e40da2c0df78 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class FactoryBeanTests { +class FactoryBeanTests { private static final Class CLASS = FactoryBeanTests.class; private static final Resource RETURNS_NULL_CONTEXT = qualifiedResource(CLASS, "returnsNull.xml"); @@ -48,7 +48,7 @@ public class FactoryBeanTests { @Test - public void testFactoryBeanReturnsNull() throws Exception { + void testFactoryBeanReturnsNull() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions(RETURNS_NULL_CONTEXT); @@ -56,7 +56,7 @@ public void testFactoryBeanReturnsNull() throws Exception { } @Test - public void testFactoryBeansWithAutowiring() throws Exception { + void testFactoryBeansWithAutowiring() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions(WITH_AUTOWIRING_CONTEXT); @@ -77,7 +77,7 @@ public void testFactoryBeansWithAutowiring() throws Exception { } @Test - public void testFactoryBeansWithIntermediateFactoryBeanAutowiringFailure() throws Exception { + void testFactoryBeansWithIntermediateFactoryBeanAutowiringFailure() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions(WITH_AUTOWIRING_CONTEXT); @@ -92,21 +92,21 @@ public void testFactoryBeansWithIntermediateFactoryBeanAutowiringFailure() throw } @Test - public void testAbstractFactoryBeanViaAnnotation() throws Exception { + void testAbstractFactoryBeanViaAnnotation() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions(ABSTRACT_CONTEXT); factory.getBeansWithAnnotation(Component.class); } @Test - public void testAbstractFactoryBeanViaType() throws Exception { + void testAbstractFactoryBeanViaType() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions(ABSTRACT_CONTEXT); factory.getBeansOfType(AbstractFactoryBean.class); } @Test - public void testCircularReferenceWithPostProcessor() { + void testCircularReferenceWithPostProcessor() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions(CIRCULAR_CONTEXT); @@ -284,11 +284,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof FactoryBean) { return bean; } - AtomicInteger c = count.get(beanName); - if (c == null) { - c = new AtomicInteger(); - count.put(beanName, c); - } + AtomicInteger c = count.computeIfAbsent(beanName, k -> new AtomicInteger()); c.incrementAndGet(); return bean; } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java b/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java index 846f6f6696c7..a72597d70577 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,10 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class Spr5475Tests { +class Spr5475Tests { @Test - public void noArgFactoryMethodInvokedWithOneArg() { + void noArgFactoryMethodInvokedWithOneArg() { assertExceptionMessageForMisconfiguredFactoryMethod( rootBeanDefinition(Foo.class) .setFactoryMethod("noArgFactory") @@ -47,7 +47,7 @@ public void noArgFactoryMethodInvokedWithOneArg() { } @Test - public void noArgFactoryMethodInvokedWithTwoArgs() { + void noArgFactoryMethodInvokedWithTwoArgs() { assertExceptionMessageForMisconfiguredFactoryMethod( rootBeanDefinition(Foo.class) .setFactoryMethod("noArgFactory") @@ -59,7 +59,7 @@ public void noArgFactoryMethodInvokedWithTwoArgs() { } @Test - public void noArgFactoryMethodInvokedWithTwoArgsAndTypesSpecified() { + void noArgFactoryMethodInvokedWithTwoArgsAndTypesSpecified() { RootBeanDefinition def = new RootBeanDefinition(Foo.class); def.setFactoryMethodName("noArgFactory"); ConstructorArgumentValues cav = new ConstructorArgumentValues(); @@ -82,7 +82,7 @@ private void assertExceptionMessageForMisconfiguredFactoryMethod(BeanDefinition } @Test - public void singleArgFactoryMethodInvokedWithNoArgs() { + void singleArgFactoryMethodInvokedWithNoArgs() { // calling a factory method that accepts arguments without any arguments emits an exception unlike cases // where a no-arg factory method is called with arguments. Adding this test just to document the difference assertExceptionMessageForMisconfiguredFactoryMethod( diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolverTests.java index 4851c1387fd7..27d071459c81 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,30 +27,30 @@ * @author Rick Evans * @author Chris Beams */ -public class AnnotationBeanWiringInfoResolverTests { +class AnnotationBeanWiringInfoResolverTests { @Test - public void testResolveWiringInfo() throws Exception { + void testResolveWiringInfo() { assertThatIllegalArgumentException().isThrownBy(() -> new AnnotationBeanWiringInfoResolver().resolveWiringInfo(null)); } @Test - public void testResolveWiringInfoWithAnInstanceOfANonAnnotatedClass() { + void testResolveWiringInfoWithAnInstanceOfANonAnnotatedClass() { AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); BeanWiringInfo info = resolver.resolveWiringInfo("java.lang.String is not @Configurable"); assertThat(info).as("Must be returning null for a non-@Configurable class instance").isNull(); } @Test - public void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClass() { + void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClass() { AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); BeanWiringInfo info = resolver.resolveWiringInfo(new Soap()); assertThat(info).as("Must *not* be returning null for a non-@Configurable class instance").isNotNull(); } @Test - public void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClassWithAutowiringTurnedOffExplicitly() { + void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClassWithAutowiringTurnedOffExplicitly() { AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); BeanWiringInfo info = resolver.resolveWiringInfo(new WirelessSoap()); assertThat(info).as("Must *not* be returning null for an @Configurable class instance even when autowiring is NO").isNotNull(); @@ -59,7 +59,7 @@ public void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClassWithAutowiringT } @Test - public void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClassWithAutowiringTurnedOffExplicitlyAndCustomBeanName() { + void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClassWithAutowiringTurnedOffExplicitlyAndCustomBeanName() { AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); BeanWiringInfo info = resolver.resolveWiringInfo(new NamedWirelessSoap()); assertThat(info).as("Must *not* be returning null for an @Configurable class instance even when autowiring is NO").isNotNull(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index 289863e6d1c8..98aa91c20308 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; @@ -32,8 +31,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; import java.util.concurrent.Callable; import java.util.function.Consumer; import java.util.function.Function; @@ -76,17 +78,20 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** + * Tests for {@link AutowiredAnnotationBeanPostProcessor}. + * * @author Juergen Hoeller * @author Mark Fisher * @author Sam Brannen * @author Chris Beams * @author Stephane Nicoll */ -public class AutowiredAnnotationBeanPostProcessorTests { +class AutowiredAnnotationBeanPostProcessorTests { private DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -116,6 +121,20 @@ void incompleteBeanDefinition() { .withRootCauseInstanceOf(IllegalStateException.class); } + @Test + void processInjection() { + ResourceInjectionBean bean = new ResourceInjectionBean(); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + bpp.processInjection(bean); + + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + } + @Test void resourceInjection() { RootBeanDefinition bd = new RootBeanDefinition(ResourceInjectionBean.class); @@ -565,15 +584,9 @@ void optionalCollectionResourceInjection() { assertThat(bean.getTestBean3()).isSameAs(tb); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getIndexedTestBean()).isSameAs(itb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb2); - assertThat(bean.nestedTestBeansSetter).hasSize(2); - assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansSetter.get(1)).isSameAs(ntb2); - assertThat(bean.nestedTestBeansField).hasSize(2); - assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansField.get(1)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb1, ntb2); + assertThat(bean.nestedTestBeansSetter).containsExactly(ntb1, ntb2); + assertThat(bean.nestedTestBeansField).containsExactly(ntb1, ntb2); } @Test @@ -596,12 +609,9 @@ void optionalCollectionResourceInjectionWithSingleElement() { assertThat(bean.getTestBean3()).isSameAs(tb); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getIndexedTestBean()).isSameAs(itb); - assertThat(bean.getNestedTestBeans()).hasSize(1); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansSetter).hasSize(1); - assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansField).hasSize(1); - assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb1); + assertThat(bean.nestedTestBeansSetter).containsExactly(ntb1); + assertThat(bean.nestedTestBeansField).containsExactly(ntb1); } @Test @@ -708,15 +718,9 @@ void orderedCollectionResourceInjection() { assertThat(bean.getTestBean3()).isSameAs(tb); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getIndexedTestBean()).isSameAs(itb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansSetter).hasSize(2); - assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb2); - assertThat(bean.nestedTestBeansSetter.get(1)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansField).hasSize(2); - assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb2); - assertThat(bean.nestedTestBeansField.get(1)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb2, ntb1); + assertThat(bean.nestedTestBeansSetter).containsExactly(ntb2, ntb1); + assertThat(bean.nestedTestBeansField).containsExactly(ntb2, ntb1); } @Test @@ -741,15 +745,9 @@ void annotationOrderedCollectionResourceInjection() { assertThat(bean.getTestBean3()).isSameAs(tb); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getIndexedTestBean()).isSameAs(itb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansSetter).hasSize(2); - assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb2); - assertThat(bean.nestedTestBeansSetter.get(1)).isSameAs(ntb1); - assertThat(bean.nestedTestBeansField).hasSize(2); - assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb2); - assertThat(bean.nestedTestBeansField.get(1)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb2, ntb1); + assertThat(bean.nestedTestBeansSetter).containsExactly(ntb2, ntb1); + assertThat(bean.nestedTestBeansField).containsExactly(ntb2, ntb1); } @Test @@ -1051,8 +1049,7 @@ void constructorResourceInjectionWithCollectionAndNullFromFactoryBean() { ConstructorsCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsCollectionResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); - assertThat(bean.getNestedTestBeans()).hasSize(1); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb2); Map map = bf.getBeansOfType(NestedTestBean.class); assertThat(map.get("nestedTestBean1")).isNull(); @@ -1073,9 +1070,7 @@ void constructorResourceInjectionWithMultipleCandidatesAsCollection() { ConstructorsCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsCollectionResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans()).contains(ntb1, ntb2); } @Test @@ -1109,9 +1104,7 @@ void constructorResourceInjectionWithMultipleCandidatesAsOrderedCollection() { ConstructorsCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsCollectionResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb2, ntb1); } @Test @@ -1126,9 +1119,7 @@ void singleConstructorInjectionWithMultipleCandidatesAsRequiredVararg() { SingleConstructorVarargBean bean = bf.getBean("annotatedBean", SingleConstructorVarargBean.class); assertThat(bean.getTestBean()).isSameAs(tb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb2, ntb1); } @Test @@ -1140,7 +1131,7 @@ void singleConstructorInjectionWithEmptyVararg() { SingleConstructorVarargBean bean = bf.getBean("annotatedBean", SingleConstructorVarargBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans()).isNotNull(); - assertThat(bean.getNestedTestBeans().isEmpty()).isTrue(); + assertThat(bean.getNestedTestBeans()).isEmpty(); } @Test @@ -1155,9 +1146,7 @@ void singleConstructorInjectionWithMultipleCandidatesAsRequiredCollection() { SingleConstructorRequiredCollectionBean bean = bf.getBean("annotatedBean", SingleConstructorRequiredCollectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb2, ntb1); } @Test @@ -1169,7 +1158,7 @@ void singleConstructorInjectionWithEmptyCollection() { SingleConstructorRequiredCollectionBean bean = bf.getBean("annotatedBean", SingleConstructorRequiredCollectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans()).isNotNull(); - assertThat(bean.getNestedTestBeans().isEmpty()).isTrue(); + assertThat(bean.getNestedTestBeans()).isEmpty(); } @Test @@ -1184,9 +1173,7 @@ void singleConstructorInjectionWithMultipleCandidatesAsOrderedCollection() { SingleConstructorOptionalCollectionBean bean = bf.getBean("annotatedBean", SingleConstructorOptionalCollectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb2, ntb1); } @Test @@ -1271,17 +1258,17 @@ void fieldInjectionWithMap() { MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); } @Test @@ -1294,14 +1281,14 @@ void methodInjectionWithMap() { MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean"); + assertThat(bean.getTestBeanMap()).containsValue(tb); assertThat(bean.getTestBean()).isSameAs(tb); bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean"); + assertThat(bean.getTestBeanMap()).containsValue(tb); assertThat(bean.getTestBean()).isSameAs(tb); } @@ -1326,8 +1313,8 @@ void methodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() { MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); TestBean tb = bf.getBean("testBean1", TestBean.class); assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsValue(tb); assertThat(bean.getTestBean()).isSameAs(tb); } @@ -1403,6 +1390,20 @@ void constructorInjectionWithPlainHashMapAsBean() { assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); } + @Test + void constructorInjectionWithSortedMapFallback() { + RootBeanDefinition bd = new RootBeanDefinition(SortedMapConstructorInjectionBean.class); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb1 = new TestBean(); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + bf.registerSingleton("testBean2", tb2); + + SortedMapConstructorInjectionBean bean = bf.getBean("annotatedBean", SortedMapConstructorInjectionBean.class); + assertThat(bean.getTestBeanMap()).containsEntry("testBean1", tb1); + assertThat(bean.getTestBeanMap()).containsEntry("testBean2", tb2); + } + @Test void constructorInjectionWithTypedSetAsBean() { RootBeanDefinition bd = new RootBeanDefinition(SetConstructorInjectionBean.class); @@ -1444,6 +1445,8 @@ void constructorInjectionWithCustomSetAsBean() { RootBeanDefinition tbs = new RootBeanDefinition(CustomCollectionFactoryMethods.class); tbs.setUniqueFactoryMethodName("testBeanSet"); bf.registerBeanDefinition("myTestBeanSet", tbs); + bf.registerSingleton("testBean1", new TestBean()); + bf.registerSingleton("testBean2", new TestBean()); CustomSetConstructorInjectionBean bean = bf.getBean("annotatedBean", CustomSetConstructorInjectionBean.class); assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); @@ -1451,6 +1454,19 @@ void constructorInjectionWithCustomSetAsBean() { assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); } + @Test + void constructorInjectionWithSortedSetFallback() { + RootBeanDefinition bd = new RootBeanDefinition(SortedSetConstructorInjectionBean.class); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb1 = new TestBean(); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + bf.registerSingleton("testBean2", tb2); + + SortedSetConstructorInjectionBean bean = bf.getBean("annotatedBean", SortedSetConstructorInjectionBean.class); + assertThat(bean.getTestBeanSet()).contains(tb1, tb2); + } + @Test void selfReference() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class)); @@ -1468,8 +1484,7 @@ void selfReferenceWithOther() { SelfInjectionBean bean = bf.getBean("annotatedBean", SelfInjectionBean.class); SelfInjectionBean bean2 = bf.getBean("annotatedBean2", SelfInjectionBean.class); assertThat(bean.reference).isSameAs(bean2); - assertThat(bean.referenceCollection).hasSize(1); - assertThat(bean.referenceCollection.get(0)).isSameAs(bean2); + assertThat(bean.referenceCollection).containsExactly(bean2); } @Test @@ -1489,8 +1504,7 @@ void selfReferenceCollectionWithOther() { SelfInjectionCollectionBean bean = bf.getBean("annotatedBean", SelfInjectionCollectionBean.class); SelfInjectionCollectionBean bean2 = bf.getBean("annotatedBean2", SelfInjectionCollectionBean.class); assertThat(bean.reference).isSameAs(bean2); - assertThat(bean2.referenceCollection).hasSize(1); - assertThat(bean.referenceCollection.get(0)).isSameAs(bean2); + assertThat(bean2.referenceCollection).containsExactly(bean2); } @Test @@ -1578,18 +1592,18 @@ void objectProviderInjectionWithPrototype() { assertThat(bean.getUniqueTestBeanWithDefault()).isEqualTo(bf.getBean("testBean")); assertThat(bean.consumeUniqueTestBean()).isEqualTo(bf.getBean("testBean")); - List testBeans = bean.iterateTestBeans(); + List testBeans = bean.iterateTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); testBeans = bean.forEachTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); testBeans = bean.streamTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); testBeans = bean.sortedTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); } @Test @@ -1606,18 +1620,18 @@ void objectProviderInjectionWithSingletonTarget() { assertThat(bean.getUniqueTestBeanWithDefault()).isSameAs(bf.getBean("testBean")); assertThat(bean.consumeUniqueTestBean()).isEqualTo(bf.getBean("testBean")); - List testBeans = bean.iterateTestBeans(); + List testBeans = bean.iterateTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); testBeans = bean.forEachTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); testBeans = bean.streamTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); testBeans = bean.sortedTestBeans(); assertThat(testBeans).hasSize(1); - assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); } @Test @@ -1634,13 +1648,13 @@ void objectProviderInjectionWithTargetNotAvailable() { assertThat(bean.consumeUniqueTestBean()).isNull(); List testBeans = bean.iterateTestBeans(); - assertThat(testBeans.isEmpty()).isTrue(); + assertThat(testBeans).isEmpty(); testBeans = bean.forEachTestBeans(); - assertThat(testBeans.isEmpty()).isTrue(); + assertThat(testBeans).isEmpty(); testBeans = bean.streamTestBeans(); - assertThat(testBeans.isEmpty()).isTrue(); + assertThat(testBeans).isEmpty(); testBeans = bean.sortedTestBeans(); - assertThat(testBeans.isEmpty()).isTrue(); + assertThat(testBeans).isEmpty(); } @Test @@ -1656,22 +1670,12 @@ void objectProviderInjectionWithTargetNotUnique() { assertThat(bean.getUniqueTestBean()).isNull(); assertThat(bean.consumeUniqueTestBean()).isNull(); - List testBeans = bean.iterateTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); - testBeans = bean.forEachTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); - testBeans = bean.streamTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); - testBeans = bean.sortedTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + TestBean testBean1 = bf.getBean("testBean1", TestBean.class); + TestBean testBean2 = bf.getBean("testBean2", TestBean.class); + assertThat(bean.iterateTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.forEachTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.streamTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.sortedTestBeans()).containsExactly(testBean1, testBean2); } @Test @@ -1687,29 +1691,19 @@ void objectProviderInjectionWithTargetPrimary() { bf.registerBeanDefinition("testBean2", tb2); ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); - assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean1")); - assertThat(bean.getOptionalTestBean()).isSameAs(bf.getBean("testBean1")); - assertThat(bean.consumeOptionalTestBean()).isSameAs(bf.getBean("testBean1")); - assertThat(bean.getUniqueTestBean()).isSameAs(bf.getBean("testBean1")); - assertThat(bean.consumeUniqueTestBean()).isSameAs(bf.getBean("testBean1")); + TestBean testBean1 = bf.getBean("testBean1", TestBean.class); + assertThat(bean.getTestBean()).isSameAs(testBean1); + assertThat(bean.getOptionalTestBean()).isSameAs(testBean1); + assertThat(bean.consumeOptionalTestBean()).isSameAs(testBean1); + assertThat(bean.getUniqueTestBean()).isSameAs(testBean1); + assertThat(bean.consumeUniqueTestBean()).isSameAs(testBean1); assertThat(bf.containsSingleton("testBean2")).isFalse(); - List testBeans = bean.iterateTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); - testBeans = bean.forEachTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); - testBeans = bean.streamTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); - testBeans = bean.sortedTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean2")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean1")); + TestBean testBean2 = bf.getBean("testBean2", TestBean.class); + assertThat(bean.iterateTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.forEachTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.streamTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.sortedTestBeans()).containsExactly(testBean2, testBean1); } @Test @@ -1725,10 +1719,8 @@ void objectProviderInjectionWithUnresolvedOrderedStream() { bf.registerBeanDefinition("testBean2", tb2); ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); - List testBeans = bean.sortedTestBeans(); - assertThat(testBeans).hasSize(2); - assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean2")); - assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean1")); + assertThat(bean.sortedTestBeans()).containsExactly(bf.getBean("testBean2", TestBean.class), + bf.getBean("testBean1", TestBean.class)); } @Test @@ -1954,10 +1946,8 @@ void genericsBasedFieldInjection() { assertThat(bean.integerArray).hasSize(1); assertThat(bean.stringArray[0]).isSameAs(sv); assertThat(bean.integerArray[0]).isSameAs(iv); - assertThat(bean.stringList).hasSize(1); - assertThat(bean.integerList).hasSize(1); - assertThat(bean.stringList.get(0)).isSameAs(sv); - assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringList).containsExactly(sv); + assertThat(bean.integerList).containsExactly(iv); assertThat(bean.stringMap).hasSize(1); assertThat(bean.integerMap).hasSize(1); assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); @@ -1968,10 +1958,8 @@ void genericsBasedFieldInjection() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(ir); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2000,10 +1988,8 @@ void genericsBasedFieldInjectionWithSubstitutedVariables() { assertThat(bean.integerArray).hasSize(1); assertThat(bean.stringArray[0]).isSameAs(sv); assertThat(bean.integerArray[0]).isSameAs(iv); - assertThat(bean.stringList).hasSize(1); - assertThat(bean.integerList).hasSize(1); - assertThat(bean.stringList.get(0)).isSameAs(sv); - assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringList).containsExactly(sv); + assertThat(bean.integerList).containsExactly(iv); assertThat(bean.stringMap).hasSize(1); assertThat(bean.integerMap).hasSize(1); assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); @@ -2014,10 +2000,8 @@ void genericsBasedFieldInjectionWithSubstitutedVariables() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(ir); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2042,10 +2026,8 @@ void genericsBasedFieldInjectionWithQualifiers() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(ir); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2078,18 +2060,12 @@ void genericsBasedFieldInjectionWithMocks() { Repository ir = bf.getBean("integerRepository", Repository.class); assertThat(bean.stringRepository).isSameAs(sr); assertThat(bean.integerRepository).isSameAs(ir); - assertThat(bean.stringRepositoryArray).hasSize(1); - assertThat(bean.integerRepositoryArray).hasSize(1); - assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); - assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); - assertThat(bean.stringRepositoryMap).hasSize(1); - assertThat(bean.integerRepositoryMap).hasSize(1); - assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); - assertThat(bean.integerRepositoryMap.get("integerRepository")).isSameAs(ir); + assertThat(bean.stringRepositoryArray).singleElement().isSameAs(sr); + assertThat(bean.integerRepositoryArray).singleElement().isSameAs(ir); + assertThat(bean.stringRepositoryList).singleElement().isSameAs(sr); + assertThat(bean.integerRepositoryList).singleElement().isSameAs(ir); + assertThat(bean.stringRepositoryMap).containsOnly(entry("stringRepo", sr)); + assertThat(bean.integerRepositoryMap).containsOnly(entry("integerRepository", ir)); } @Test @@ -2105,17 +2081,14 @@ void genericsBasedFieldInjectionWithSimpleMatch() { Repository repo = bf.getBean("repo", Repository.class); assertThat(bean.repository).isSameAs(repo); assertThat(bean.stringRepository).isSameAs(repo); - assertThat(bean.repositoryArray).hasSize(1); + assertThat(bean.repositoryArray).containsExactly(repo); assertThat(bean.stringRepositoryArray).hasSize(1); - assertThat(bean.repositoryArray[0]).isSameAs(repo); assertThat(bean.stringRepositoryArray[0]).isSameAs(repo); - assertThat(bean.repositoryList).hasSize(1); + assertThat(bean.repositoryList).containsExactly(repo); assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.repositoryList.get(0)).isSameAs(repo); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(repo); - assertThat(bean.repositoryMap).hasSize(1); + assertThat(bean.stringRepositoryList).element(0).isSameAs(repo); + assertThat(bean.repositoryMap).containsOnly(entry("repo", repo)); assertThat(bean.stringRepositoryMap).hasSize(1); - assertThat(bean.repositoryMap.get("repo")).isSameAs(repo); assertThat(bean.stringRepositoryMap.get("repo")).isSameAs(repo); assertThat(bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class))).isEqualTo(new String[] {"repo"}); @@ -2169,8 +2142,7 @@ void genericsBasedFieldInjectionWithSimpleMatchAndMock() { assertThat(bean.stringRepositoryArray[0]).isSameAs(repo); assertThat(bean.repositoryList).hasSize(1); assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.repositoryList.get(0)).isSameAs(repo); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(repo); + assertThat(bean.stringRepositoryList).element(0).isSameAs(repo); assertThat(bean.repositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.repositoryMap.get("repo")).isSameAs(repo); @@ -2198,10 +2170,9 @@ void genericsBasedFieldInjectionWithSimpleMatchAndMockito() { assertThat(bean.stringRepositoryArray).hasSize(1); assertThat(bean.repositoryArray[0]).isSameAs(repo); assertThat(bean.stringRepositoryArray[0]).isSameAs(repo); - assertThat(bean.repositoryList).hasSize(1); + assertThat(bean.repositoryList).containsExactly(repo); assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.repositoryList.get(0)).isSameAs(repo); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(repo); + assertThat(bean.stringRepositoryList).element(0).isSameAs(repo); assertThat(bean.repositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.repositoryMap.get("repo")).isSameAs(repo); @@ -2238,10 +2209,8 @@ void genericsBasedMethodInjection() { assertThat(bean.integerArray).hasSize(1); assertThat(bean.stringArray[0]).isSameAs(sv); assertThat(bean.integerArray[0]).isSameAs(iv); - assertThat(bean.stringList).hasSize(1); - assertThat(bean.integerList).hasSize(1); - assertThat(bean.stringList.get(0)).isSameAs(sv); - assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringList).containsExactly(sv); + assertThat(bean.integerList).containsExactly(iv); assertThat(bean.stringMap).hasSize(1); assertThat(bean.integerMap).hasSize(1); assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); @@ -2252,10 +2221,8 @@ void genericsBasedMethodInjection() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(ir); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2284,10 +2251,8 @@ void genericsBasedMethodInjectionWithSubstitutedVariables() { assertThat(bean.integerArray).hasSize(1); assertThat(bean.stringArray[0]).isSameAs(sv); assertThat(bean.integerArray[0]).isSameAs(iv); - assertThat(bean.stringList).hasSize(1); - assertThat(bean.integerList).hasSize(1); - assertThat(bean.stringList.get(0)).isSameAs(sv); - assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringList).containsExactly(sv); + assertThat(bean.integerList).containsExactly(iv); assertThat(bean.stringMap).hasSize(1); assertThat(bean.integerMap).hasSize(1); assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); @@ -2298,10 +2263,8 @@ void genericsBasedMethodInjectionWithSubstitutedVariables() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(ir); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2325,10 +2288,8 @@ void genericsBasedConstructorInjection() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(ir); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2351,10 +2312,8 @@ void genericsBasedConstructorInjectionWithNonTypedTarget() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(gr); assertThat(bean.integerRepositoryArray[0]).isSameAs(gr); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(gr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(gr); + assertThat(bean.stringRepositoryList).containsExactly(gr); + assertThat(bean.integerRepositoryList).containsExactly(gr); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("genericRepo")).isSameAs(gr); @@ -2376,10 +2335,8 @@ void genericsBasedConstructorInjectionWithNonGenericTarget() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(ngr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ngr); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(ngr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ngr); + assertThat(bean.stringRepositoryList).containsExactly(ngr); + assertThat(bean.integerRepositoryList).containsExactly(ngr); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("simpleRepo")).isSameAs(ngr); @@ -2404,10 +2361,8 @@ void genericsBasedConstructorInjectionWithMixedTargets() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(gr); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(gr); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(gr); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2431,10 +2386,8 @@ void genericsBasedConstructorInjectionWithMixedTargetsIncludingNonGeneric() { assertThat(bean.integerRepositoryArray).hasSize(1); assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); assertThat(bean.integerRepositoryArray[0]).isSameAs(ngr); - assertThat(bean.stringRepositoryList).hasSize(1); - assertThat(bean.integerRepositoryList).hasSize(1); - assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); - assertThat(bean.integerRepositoryList.get(0)).isSameAs(ngr); + assertThat(bean.stringRepositoryList).containsExactly(sr); + assertThat(bean.integerRepositoryList).containsExactly(ngr); assertThat(bean.stringRepositoryMap).hasSize(1); assertThat(bean.integerRepositoryMap).hasSize(1); assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); @@ -2569,23 +2522,41 @@ void factoryBeanSelfInjectionViaFactoryMethod() { assertThat(bean.testBean).isSameAs(bf.getBean("annotatedBean")); } - private Consumer methodParameterDeclaredOn( - Class expected) { + @Test + void mixedNullableArgMethodInjection(){ + bf.registerSingleton("nonNullBean", "Test"); + bf.registerBeanDefinition("mixedNullableInjectionBean", + new RootBeanDefinition(MixedNullableInjectionBean.class)); + MixedNullableInjectionBean mixedNullableInjectionBean = bf.getBean(MixedNullableInjectionBean.class); + assertThat(mixedNullableInjectionBean.nonNullBean).isNotNull(); + assertThat(mixedNullableInjectionBean.nullableBean).isNull(); + } + + @Test + void mixedOptionalArgMethodInjection(){ + bf.registerSingleton("nonNullBean", "Test"); + bf.registerBeanDefinition("mixedOptionalInjectionBean", + new RootBeanDefinition(MixedOptionalInjectionBean.class)); + MixedOptionalInjectionBean mixedOptionalInjectionBean = bf.getBean(MixedOptionalInjectionBean.class); + assertThat(mixedOptionalInjectionBean.nonNullBean).isNotNull(); + assertThat(mixedOptionalInjectionBean.nullableBean).isNull(); + } + + + private Consumer methodParameterDeclaredOn(Class expected) { return declaredOn( injectionPoint -> injectionPoint.getMethodParameter().getDeclaringClass(), expected); } - private Consumer fieldDeclaredOn( - Class expected) { + private Consumer fieldDeclaredOn(Class expected) { return declaredOn( injectionPoint -> injectionPoint.getField().getDeclaringClass(), expected); } private Consumer declaredOn( - Function> declaringClassExtractor, - Class expected) { + Function> declaringClassExtractor, Class expected) { return ex -> { InjectionPoint injectionPoint = ex.getInjectionPoint(); Class declaringClass = declaringClassExtractor.apply(injectionPoint); @@ -3116,6 +3087,21 @@ public Map getTestBeanMap() { } + public static class SortedMapConstructorInjectionBean { + + private SortedMap testBeanMap; + + @Autowired + public SortedMapConstructorInjectionBean(SortedMap testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public SortedMap getTestBeanMap() { + return this.testBeanMap; + } + } + + public static class QualifiedMapConstructorInjectionBean { private Map testBeanMap; @@ -3146,6 +3132,21 @@ public Set getTestBeanSet() { } + public static class SortedSetConstructorInjectionBean { + + private SortedSet testBeanSet; + + @Autowired + public SortedSetConstructorInjectionBean(SortedSet testBeanSet) { + this.testBeanSet = testBeanSet; + } + + public SortedSet getTestBeanSet() { + return this.testBeanSet; + } + } + + public static class SelfInjectionBean { @Autowired @@ -3381,7 +3382,7 @@ public final FactoryBean getFactoryBean() { public static class StringFactoryBean implements FactoryBean { @Override - public String getObject() throws Exception { + public String getObject() { return ""; } @@ -3916,7 +3917,7 @@ public static class MocksControl { @SuppressWarnings("unchecked") public T createMock(Class toMock) { return (T) Proxy.newProxyInstance(AutowiredAnnotationBeanPostProcessorTests.class.getClassLoader(), new Class[] {toMock}, - (InvocationHandler) (proxy, method, args) -> { + (proxy, method, args) -> { throw new UnsupportedOperationException("mocked!"); }); } @@ -4064,7 +4065,7 @@ public static class StockServiceImpl { public static class MyCallable implements Callable { @Override - public Thread call() throws Exception { + public Thread call() { return null; } } @@ -4073,13 +4074,13 @@ public Thread call() throws Exception { public static class SecondCallable implements Callable{ @Override - public Thread call() throws Exception { + public Thread call() { return null; } } - public static abstract class Foo> { + public abstract static class Foo> { private RT obj; @@ -4285,4 +4286,34 @@ public static TestBean newTestBean2() { } } + + static class MixedNullableInjectionBean { + + @Nullable + public Integer nullableBean; + + public String nonNullBean; + + @Autowired(required = false) + public void nullabilityInjection(@Nullable Integer nullableBean, String nonNullBean) { + this.nullableBean = nullableBean; + this.nonNullBean = nonNullBean; + } + } + + + static class MixedOptionalInjectionBean { + + @Nullable + public Integer nullableBean; + + public String nonNullBean; + + @Autowired(required = false) + public void optionalInjection(Optional optionalBean, String nonNullBean) { + optionalBean.ifPresent(bean -> this.nullableBean = bean); + this.nonNullBean = nonNullBean; + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java index 96b9ae449209..0e38a02da29c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import javax.lang.model.element.Modifier; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.MethodReference; @@ -28,9 +29,17 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.CodeWarnings; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.annotation.DeprecatedInjectionSamples.DeprecatedFieldInjectionPointSample; +import org.springframework.beans.testfixture.beans.factory.annotation.DeprecatedInjectionSamples.DeprecatedFieldInjectionTypeSample; +import org.springframework.beans.testfixture.beans.factory.annotation.DeprecatedInjectionSamples.DeprecatedMethodInjectionPointSample; +import org.springframework.beans.testfixture.beans.factory.annotation.DeprecatedInjectionSamples.DeprecatedMethodInjectionTypeSample; +import org.springframework.beans.testfixture.beans.factory.annotation.DeprecatedInjectionSamples.DeprecatedPrivateFieldInjectionTypeSample; +import org.springframework.beans.testfixture.beans.factory.annotation.DeprecatedInjectionSamples.DeprecatedPrivateMethodInjectionTypeSample; +import org.springframework.beans.testfixture.beans.factory.annotation.DeprecatedInjectionSamples.DeprecatedSample; import org.springframework.beans.testfixture.beans.factory.annotation.PackagePrivateFieldInjectionSample; import org.springframework.beans.testfixture.beans.factory.annotation.PackagePrivateMethodInjectionSample; import org.springframework.beans.testfixture.beans.factory.annotation.PrivateFieldInjectionSample; @@ -49,6 +58,7 @@ import org.springframework.javapoet.ParameterizedTypeName; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link AutowiredAnnotationBeanPostProcessor} for AOT contributions. @@ -199,6 +209,69 @@ void contributeWhenMethodInjectionHasMatchingPropertyValue() { assertThat(contribution).isNull(); } + @Nested + @SuppressWarnings("deprecation") + class DeprecationTests { + + private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem() + .withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror"); + + @Test + void contributeWhenTargetClassIsDeprecated() { + RegisteredBean registeredBean = getAndApplyContribution(DeprecatedSample.class); + compileAndCheckWarnings(registeredBean); + } + + @Test + void contributeWhenFieldInjectionsUsesADeprecatedType() { + RegisteredBean registeredBean = getAndApplyContribution( + DeprecatedFieldInjectionTypeSample.class); + compileAndCheckWarnings(registeredBean); + } + + @Test + void contributeWhenFieldInjectionsUsesADeprecatedTypeWithReflection() { + RegisteredBean registeredBean = getAndApplyContribution( + DeprecatedPrivateFieldInjectionTypeSample.class); + compileAndCheckWarnings(registeredBean); + } + + @Test + void contributeWhenFieldInjectionsIsDeprecated() { + RegisteredBean registeredBean = getAndApplyContribution( + DeprecatedFieldInjectionPointSample.class); + compileAndCheckWarnings(registeredBean); + } + + @Test + void contributeWhenMethodInjectionsUsesADeprecatedType() { + RegisteredBean registeredBean = getAndApplyContribution( + DeprecatedMethodInjectionTypeSample.class); + compileAndCheckWarnings(registeredBean); + } + + @Test + void contributeWhenMethodInjectionsUsesADeprecatedTypeWithReflection() { + RegisteredBean registeredBean = getAndApplyContribution( + DeprecatedPrivateMethodInjectionTypeSample.class); + compileAndCheckWarnings(registeredBean); + } + + @Test + void contributeWhenMethodInjectionsIsDeprecated() { + RegisteredBean registeredBean = getAndApplyContribution( + DeprecatedMethodInjectionPointSample.class); + compileAndCheckWarnings(registeredBean); + } + + + private void compileAndCheckWarnings(RegisteredBean registeredBean) { + assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, registeredBean, + ((instanceSupplier, compiled) -> {}))); + } + + } + private RegisteredBean getAndApplyContribution(Class beanClass) { RegisteredBean registeredBean = registerBean(beanClass); BeanRegistrationAotContribution contribution = this.beanPostProcessor.processAheadOfTime(registeredBean); @@ -218,11 +291,17 @@ private static SourceFile getSourceFile(Compiled compiled, Class sample) { return compiled.getSourceFileFromPackage(sample.getPackageName()); } - @SuppressWarnings("unchecked") private void compile(RegisteredBean registeredBean, BiConsumer, Compiled> result) { + compile(TestCompiler.forSystem(), registeredBean, result); + } + + @SuppressWarnings("unchecked") + private void compile(TestCompiler testCompiler, RegisteredBean registeredBean, + BiConsumer, Compiled> result) { Class target = registeredBean.getBeanClass(); MethodReference methodReference = this.beanRegistrationCode.getInstancePostProcessors().get(0); + CodeWarnings codeWarnings = new CodeWarnings(); this.beanRegistrationCode.getTypeBuilder().set(type -> { CodeBlock methodInvocation = methodReference.toInvokeCodeBlock( ArgumentCodeGenerator.of(RegisteredBean.class, "registeredBean").and(target, "instance"), @@ -235,10 +314,11 @@ private void compile(RegisteredBean registeredBean, .addParameter(target, "instance").returns(target) .addStatement("return $L", methodInvocation) .build()); - + codeWarnings.detectDeprecation(target); + codeWarnings.suppress(type); }); this.generationContext.writeGeneratedContent(); - TestCompiler.forSystem().with(this.generationContext).compile(compiled -> + testCompiler.with(this.generationContext).printFiles(System.out).compile(compiled -> result.accept(compiled.getInstance(BiFunction.class), compiled)); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests.java index 02a87f6c0d4b..526397981fca 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,16 @@ import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; /** - * Unit tests for {@link CustomAutowireConfigurer}. + * Tests for {@link CustomAutowireConfigurer}. * * @author Mark Fisher * @author Juergen Hoeller * @author Chris Beams */ -public class CustomAutowireConfigurerTests { +class CustomAutowireConfigurerTests { @Test - public void testCustomResolver() { + void testCustomResolver() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(CustomAutowireConfigurerTests.class, "context.xml")); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java index e94e472d0af1..ba6ff33640ee 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessorTests.StringFactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.AutowireCandidateQualifier; @@ -49,13 +50,13 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Unit tests for {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor} - * processing the JSR-330 {@link jakarta.inject.Inject} annotation. + * Tests for {@link AutowiredAnnotationBeanPostProcessor} processing + * the JSR-330 {@link jakarta.inject.Inject} annotation. * * @author Juergen Hoeller * @since 3.0 */ -public class InjectAnnotationBeanPostProcessorTests { +class InjectAnnotationBeanPostProcessorTests { private DefaultListableBeanFactory bf; @@ -63,7 +64,7 @@ public class InjectAnnotationBeanPostProcessorTests { @BeforeEach - public void setup() { + void setup() { bf = new DefaultListableBeanFactory(); bf.registerResolvableDependency(BeanFactory.class, bf); bpp = new AutowiredAnnotationBeanPostProcessor(); @@ -73,24 +74,24 @@ public void setup() { } @AfterEach - public void close() { + void close() { bf.destroySingletons(); } @Test - public void testIncompleteBeanDefinition() { + void testIncompleteBeanDefinition() { bf.registerBeanDefinition("testBean", new GenericBeanDefinition()); try { bf.getBean("testBean"); } catch (BeanCreationException ex) { - assertThat(ex.getRootCause() instanceof IllegalStateException).isTrue(); + assertThat(ex.getRootCause()).isInstanceOf(IllegalStateException.class); } } @Test - public void testResourceInjection() { + void testResourceInjection() { RootBeanDefinition bd = new RootBeanDefinition(ResourceInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -107,7 +108,7 @@ public void testResourceInjection() { } @Test - public void testExtendedResourceInjection() { + void testExtendedResourceInjection() { RootBeanDefinition bd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -134,7 +135,7 @@ public void testExtendedResourceInjection() { } @Test - public void testExtendedResourceInjectionWithOverriding() { + void testExtendedResourceInjectionWithOverriding() { RootBeanDefinition annotatedBd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); TestBean tb2 = new TestBean(); annotatedBd.getPropertyValues().add("testBean2", tb2); @@ -154,7 +155,7 @@ public void testExtendedResourceInjectionWithOverriding() { } @Test - public void testConstructorResourceInjection() { + void testConstructorResourceInjection() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -181,7 +182,7 @@ public void testConstructorResourceInjection() { } @Test - public void testConstructorResourceInjectionWithMultipleCandidatesAsCollection() { + void testConstructorResourceInjectionWithMultipleCandidatesAsCollection() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsCollectionResourceInjectionBean.class)); TestBean tb = new TestBean(); @@ -194,13 +195,11 @@ public void testConstructorResourceInjectionWithMultipleCandidatesAsCollection() ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); - assertThat(bean.getNestedTestBeans()).hasSize(2); - assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); - assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans()).containsExactly(ntb1, ntb2); } @Test - public void testConstructorResourceInjectionWithMultipleCandidatesAndFallback() { + void testConstructorResourceInjectionWithMultipleCandidatesAndFallback() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsResourceInjectionBean.class)); TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); @@ -211,7 +210,7 @@ public void testConstructorResourceInjectionWithMultipleCandidatesAndFallback() } @Test - public void testConstructorInjectionWithMap() { + void testConstructorInjectionWithMap() { RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -222,21 +221,21 @@ public void testConstructorInjectionWithMap() { MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); } @Test - public void testFieldInjectionWithMap() { + void testFieldInjectionWithMap() { RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -247,21 +246,21 @@ public void testFieldInjectionWithMap() { MapFieldInjectionBean bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); } @Test - public void testMethodInjectionWithMap() { + void testMethodInjectionWithMap() { RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -270,19 +269,19 @@ public void testMethodInjectionWithMap() { MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean"); + assertThat(bean.getTestBeanMap()).containsValue(tb); assertThat(bean.getTestBean()).isSameAs(tb); bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean"); + assertThat(bean.getTestBeanMap()).containsValue(tb); assertThat(bean.getTestBean()).isSameAs(tb); } @Test - public void testMethodInjectionWithMapAndMultipleMatches() { + void testMethodInjectionWithMapAndMultipleMatches() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); @@ -291,7 +290,7 @@ public void testMethodInjectionWithMapAndMultipleMatches() { } @Test - public void testMethodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() { + void testMethodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class); @@ -301,13 +300,13 @@ public void testMethodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandid MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); TestBean tb = (TestBean) bf.getBean("testBean1"); assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); - assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsValue(tb); assertThat(bean.getTestBean()).isSameAs(tb); } @Test - public void testObjectFactoryInjection() { + void testObjectFactoryInjection() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryQualifierFieldInjectionBean.class)); RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "testBean")); @@ -319,7 +318,7 @@ public void testObjectFactoryInjection() { } @Test - public void testObjectFactoryQualifierInjection() { + void testObjectFactoryQualifierInjection() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryQualifierFieldInjectionBean.class)); RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "testBean")); @@ -330,7 +329,7 @@ public void testObjectFactoryQualifierInjection() { } @Test - public void testObjectFactoryFieldInjectionIntoPrototypeBean() { + void testObjectFactoryFieldInjectionIntoPrototypeBean() { RootBeanDefinition annotatedBeanDefinition = new RootBeanDefinition(ObjectFactoryQualifierFieldInjectionBean.class); annotatedBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", annotatedBeanDefinition); @@ -347,7 +346,7 @@ public void testObjectFactoryFieldInjectionIntoPrototypeBean() { } @Test - public void testObjectFactoryMethodInjectionIntoPrototypeBean() { + void testObjectFactoryMethodInjectionIntoPrototypeBean() { RootBeanDefinition annotatedBeanDefinition = new RootBeanDefinition(ObjectFactoryQualifierMethodInjectionBean.class); annotatedBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", annotatedBeanDefinition); @@ -364,7 +363,7 @@ public void testObjectFactoryMethodInjectionIntoPrototypeBean() { } @Test - public void testObjectFactoryWithBeanField() throws Exception { + void testObjectFactoryWithBeanField() throws Exception { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); bf.setSerializationId("test"); @@ -376,7 +375,7 @@ public void testObjectFactoryWithBeanField() throws Exception { } @Test - public void testObjectFactoryWithBeanMethod() throws Exception { + void testObjectFactoryWithBeanMethod() throws Exception { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); bf.setSerializationId("test"); @@ -388,7 +387,7 @@ public void testObjectFactoryWithBeanMethod() throws Exception { } @Test - public void testObjectFactoryWithTypedListField() throws Exception { + void testObjectFactoryWithTypedListField() throws Exception { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryListFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); bf.setSerializationId("test"); @@ -400,7 +399,7 @@ public void testObjectFactoryWithTypedListField() throws Exception { } @Test - public void testObjectFactoryWithTypedListMethod() throws Exception { + void testObjectFactoryWithTypedListMethod() throws Exception { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryListMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); bf.setSerializationId("test"); @@ -412,7 +411,7 @@ public void testObjectFactoryWithTypedListMethod() throws Exception { } @Test - public void testObjectFactoryWithTypedMapField() throws Exception { + void testObjectFactoryWithTypedMapField() throws Exception { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMapFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); bf.setSerializationId("test"); @@ -424,7 +423,7 @@ public void testObjectFactoryWithTypedMapField() throws Exception { } @Test - public void testObjectFactoryWithTypedMapMethod() throws Exception { + void testObjectFactoryWithTypedMapMethod() throws Exception { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMapMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); bf.setSerializationId("test"); @@ -441,7 +440,7 @@ public void testObjectFactoryWithTypedMapMethod() throws Exception { * specifically addressing SPR-4040. */ @Test - public void testBeanAutowiredWithFactoryBean() { + void testBeanAutowiredWithFactoryBean() { bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class)); bf.registerSingleton("stringFactoryBean", new StringFactoryBean()); @@ -454,7 +453,7 @@ public void testBeanAutowiredWithFactoryBean() { } @Test - public void testNullableFieldInjectionWithBeanAvailable() { + void testNullableFieldInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -463,7 +462,7 @@ public void testNullableFieldInjectionWithBeanAvailable() { } @Test - public void testNullableFieldInjectionWithBeanNotAvailable() { + void testNullableFieldInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class)); NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean"); @@ -471,7 +470,7 @@ public void testNullableFieldInjectionWithBeanNotAvailable() { } @Test - public void testNullableMethodInjectionWithBeanAvailable() { + void testNullableMethodInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -480,7 +479,7 @@ public void testNullableMethodInjectionWithBeanAvailable() { } @Test - public void testNullableMethodInjectionWithBeanNotAvailable() { + void testNullableMethodInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class)); NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean"); @@ -488,7 +487,7 @@ public void testNullableMethodInjectionWithBeanNotAvailable() { } @Test - public void testOptionalFieldInjectionWithBeanAvailable() { + void testOptionalFieldInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -498,7 +497,7 @@ public void testOptionalFieldInjectionWithBeanAvailable() { } @Test - public void testOptionalFieldInjectionWithBeanNotAvailable() { + void testOptionalFieldInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalFieldInjectionBean.class)); OptionalFieldInjectionBean bean = (OptionalFieldInjectionBean) bf.getBean("annotatedBean"); @@ -506,7 +505,7 @@ public void testOptionalFieldInjectionWithBeanNotAvailable() { } @Test - public void testOptionalMethodInjectionWithBeanAvailable() { + void testOptionalMethodInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -516,7 +515,7 @@ public void testOptionalMethodInjectionWithBeanAvailable() { } @Test - public void testOptionalMethodInjectionWithBeanNotAvailable() { + void testOptionalMethodInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalMethodInjectionBean.class)); OptionalMethodInjectionBean bean = (OptionalMethodInjectionBean) bf.getBean("annotatedBean"); @@ -524,17 +523,17 @@ public void testOptionalMethodInjectionWithBeanNotAvailable() { } @Test - public void testOptionalListFieldInjectionWithBeanAvailable() { + void testOptionalListFieldInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); OptionalListFieldInjectionBean bean = (OptionalListFieldInjectionBean) bf.getBean("annotatedBean"); - assertThat(bean.getTestBean()).isPresent(); - assertThat(bean.getTestBean().get().get(0)).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean()).hasValueSatisfying(list -> + assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class))); } @Test - public void testOptionalListFieldInjectionWithBeanNotAvailable() { + void testOptionalListFieldInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListFieldInjectionBean.class)); OptionalListFieldInjectionBean bean = (OptionalListFieldInjectionBean) bf.getBean("annotatedBean"); @@ -542,17 +541,17 @@ public void testOptionalListFieldInjectionWithBeanNotAvailable() { } @Test - public void testOptionalListMethodInjectionWithBeanAvailable() { + void testOptionalListMethodInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); OptionalListMethodInjectionBean bean = (OptionalListMethodInjectionBean) bf.getBean("annotatedBean"); - assertThat(bean.getTestBean()).isPresent(); - assertThat(bean.getTestBean().get().get(0)).isSameAs(bf.getBean("testBean")); + assertThat(bean.getTestBean()).hasValueSatisfying(list -> + assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class))); } @Test - public void testOptionalListMethodInjectionWithBeanNotAvailable() { + void testOptionalListMethodInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListMethodInjectionBean.class)); OptionalListMethodInjectionBean bean = (OptionalListMethodInjectionBean) bf.getBean("annotatedBean"); @@ -560,7 +559,7 @@ public void testOptionalListMethodInjectionWithBeanNotAvailable() { } @Test - public void testProviderOfOptionalFieldInjectionWithBeanAvailable() { + void testProviderOfOptionalFieldInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -570,7 +569,7 @@ public void testProviderOfOptionalFieldInjectionWithBeanAvailable() { } @Test - public void testProviderOfOptionalFieldInjectionWithBeanNotAvailable() { + void testProviderOfOptionalFieldInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalFieldInjectionBean.class)); ProviderOfOptionalFieldInjectionBean bean = (ProviderOfOptionalFieldInjectionBean) bf.getBean("annotatedBean"); @@ -578,7 +577,7 @@ public void testProviderOfOptionalFieldInjectionWithBeanNotAvailable() { } @Test - public void testProviderOfOptionalMethodInjectionWithBeanAvailable() { + void testProviderOfOptionalMethodInjectionWithBeanAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -588,7 +587,7 @@ public void testProviderOfOptionalMethodInjectionWithBeanAvailable() { } @Test - public void testProviderOfOptionalMethodInjectionWithBeanNotAvailable() { + void testProviderOfOptionalMethodInjectionWithBeanNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalMethodInjectionBean.class)); ProviderOfOptionalMethodInjectionBean bean = (ProviderOfOptionalMethodInjectionBean) bf.getBean("annotatedBean"); @@ -596,7 +595,7 @@ public void testProviderOfOptionalMethodInjectionWithBeanNotAvailable() { } @Test - public void testAnnotatedDefaultConstructor() { + void testAnnotatedDefaultConstructor() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedDefaultConstructorBean.class)); assertThat(bf.getBean("annotatedBean")).isNotNull(); @@ -644,7 +643,6 @@ public ExtendedResourceInjectionBean() { @Override @Inject - @SuppressWarnings("deprecation") public void setTestBean2(TestBean testBean2) { super.setTestBean2(testBean2); } @@ -1108,25 +1106,6 @@ public final FactoryBean getFactoryBean() { } - public static class StringFactoryBean implements FactoryBean { - - @Override - public String getObject() { - return ""; - } - - @Override - public Class getObjectType() { - return String.class; - } - - @Override - public boolean isSingleton() { - return true; - } - } - - @Retention(RetentionPolicy.RUNTIME) public @interface Nullable {} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java index d56b851f93f3..ef2e236fb689 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import jakarta.inject.Inject; +import jakarta.inject.Provider; import jakarta.inject.Qualifier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,6 +35,7 @@ * Tests for {@link JakartaAnnotationsRuntimeHints}. * * @author Brian Clozel + * @author Sam Brannen */ class JakartaAnnotationsRuntimeHintsTests { @@ -51,9 +53,24 @@ void jakartaInjectAnnotationHasHints() { assertThat(RuntimeHintsPredicates.reflection().onType(Inject.class)).accepts(this.hints); } + @Test + void jakartaProviderAnnotationHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(Provider.class)).accepts(this.hints); + } + @Test void jakartaQualifierAnnotationHasHints() { assertThat(RuntimeHintsPredicates.reflection().onType(Qualifier.class)).accepts(this.hints); } + @Test // gh-33345 + void javaxInjectAnnotationHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(javax.inject.Inject.class)).accepts(this.hints); + } + + @Test // gh-33345 + void javaxQualifierAnnotationHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(javax.inject.Qualifier.class)).accepts(this.hints); + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java index 52892821a6dc..f12e9a0fac64 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,13 +31,13 @@ * @author Karl Pietrzak * @author Juergen Hoeller */ -public class LookupAnnotationTests { +class LookupAnnotationTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setup() { + void setup() { beanFactory = new DefaultListableBeanFactory(); AutowiredAnnotationBeanPostProcessor aabpp = new AutowiredAnnotationBeanPostProcessor(); aabpp.setBeanFactory(beanFactory); @@ -51,7 +51,7 @@ public void setup() { @Test - public void testWithoutConstructorArg() { + void testWithoutConstructorArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); Object expected = bean.get(); assertThat(expected.getClass()).isEqualTo(TestBean.class); @@ -59,7 +59,7 @@ public void testWithoutConstructorArg() { } @Test - public void testWithOverloadedArg() { + void testWithOverloadedArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); TestBean expected = bean.get("haha"); assertThat(expected.getClass()).isEqualTo(TestBean.class); @@ -68,7 +68,7 @@ public void testWithOverloadedArg() { } @Test - public void testWithOneConstructorArg() { + void testWithOneConstructorArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); TestBean expected = bean.getOneArgument("haha"); assertThat(expected.getClass()).isEqualTo(TestBean.class); @@ -77,7 +77,7 @@ public void testWithOneConstructorArg() { } @Test - public void testWithTwoConstructorArg() { + void testWithTwoConstructorArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); TestBean expected = bean.getTwoArguments("haha", 72); assertThat(expected.getClass()).isEqualTo(TestBean.class); @@ -87,7 +87,7 @@ public void testWithTwoConstructorArg() { } @Test - public void testWithThreeArgsShouldFail() { + void testWithThreeArgsShouldFail() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); assertThatExceptionOfType(AbstractMethodError.class).as("TestBean has no three arg constructor").isThrownBy(() -> bean.getThreeArguments("name", 1, 2)); @@ -95,7 +95,7 @@ public void testWithThreeArgsShouldFail() { } @Test - public void testWithEarlyInjection() { + void testWithEarlyInjection() { AbstractBean bean = beanFactory.getBean("beanConsumer", BeanConsumer.class).abstractBean; Object expected = bean.get(); assertThat(expected.getClass()).isEqualTo(TestBean.class); @@ -115,7 +115,7 @@ public void testWithNullBean() { } @Test - public void testWithGenericBean() { + void testWithGenericBean() { beanFactory.registerBeanDefinition("numberBean", new RootBeanDefinition(NumberBean.class)); beanFactory.registerBeanDefinition("doubleStore", new RootBeanDefinition(DoubleStore.class)); beanFactory.registerBeanDefinition("floatStore", new RootBeanDefinition(FloatStore.class)); @@ -125,8 +125,38 @@ public void testWithGenericBean() { assertThat(beanFactory.getBean(FloatStore.class)).isSameAs(bean.getFloatStore()); } + @Test + void testSingletonWithoutMetadataCaching() { + beanFactory.setCacheBeanMetadata(false); + + beanFactory.registerBeanDefinition("numberBean", new RootBeanDefinition(NumberBean.class)); + beanFactory.registerBeanDefinition("doubleStore", new RootBeanDefinition(DoubleStore.class)); + beanFactory.registerBeanDefinition("floatStore", new RootBeanDefinition(FloatStore.class)); + + NumberBean bean = (NumberBean) beanFactory.getBean("numberBean"); + assertThat(beanFactory.getBean(DoubleStore.class)).isSameAs(bean.getDoubleStore()); + assertThat(beanFactory.getBean(FloatStore.class)).isSameAs(bean.getFloatStore()); + } + + @Test + void testPrototypeWithoutMetadataCaching() { + beanFactory.setCacheBeanMetadata(false); + + beanFactory.registerBeanDefinition("numberBean", new RootBeanDefinition(NumberBean.class, BeanDefinition.SCOPE_PROTOTYPE, null)); + beanFactory.registerBeanDefinition("doubleStore", new RootBeanDefinition(DoubleStore.class)); + beanFactory.registerBeanDefinition("floatStore", new RootBeanDefinition(FloatStore.class)); + + NumberBean bean = (NumberBean) beanFactory.getBean("numberBean"); + assertThat(beanFactory.getBean(DoubleStore.class)).isSameAs(bean.getDoubleStore()); + assertThat(beanFactory.getBean(FloatStore.class)).isSameAs(bean.getFloatStore()); + + bean = (NumberBean) beanFactory.getBean("numberBean"); + assertThat(beanFactory.getBean(DoubleStore.class)).isSameAs(bean.getDoubleStore()); + assertThat(beanFactory.getBean(FloatStore.class)).isSameAs(bean.getFloatStore()); + } + - public static abstract class AbstractBean { + public abstract static class AbstractBean { @Lookup("testBean") public abstract TestBean get(); @@ -164,7 +194,7 @@ public static class FloatStore extends NumberStore { } - public static abstract class NumberBean { + public abstract static class NumberBean { @Lookup public abstract NumberStore getDoubleStore(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java index de4e74fc27a8..e2afff6ee817 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,35 +35,35 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for {@link ParameterResolutionDelegate}. + * Tests for {@link ParameterResolutionDelegate}. * * @author Sam Brannen * @author Juergen Hoeller * @author Loïc Ledoyen */ -public class ParameterResolutionTests { +class ParameterResolutionTests { @Test - public void isAutowirablePreconditions() { + void isAutowirablePreconditions() { assertThatIllegalArgumentException().isThrownBy(() -> ParameterResolutionDelegate.isAutowirable(null, 0)) .withMessageContaining("Parameter must not be null"); } @Test - public void annotatedParametersInMethodAreCandidatesForAutowiring() throws Exception { + void annotatedParametersInMethodAreCandidatesForAutowiring() throws Exception { Method method = getClass().getDeclaredMethod("autowirableMethod", String.class, String.class, String.class, String.class); assertAutowirableParameters(method); } @Test - public void annotatedParametersInTopLevelClassConstructorAreCandidatesForAutowiring() throws Exception { + void annotatedParametersInTopLevelClassConstructorAreCandidatesForAutowiring() throws Exception { Constructor constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class); assertAutowirableParameters(constructor); } @Test - public void annotatedParametersInInnerClassConstructorAreCandidatesForAutowiring() throws Exception { + void annotatedParametersInInnerClassConstructorAreCandidatesForAutowiring() throws Exception { Class innerClass = AutowirableClass.InnerAutowirableClass.class; assertThat(ClassUtils.isInnerClass(innerClass)).isTrue(); Constructor constructor = innerClass.getConstructor(AutowirableClass.class, String.class, String.class); @@ -81,7 +81,7 @@ private void assertAutowirableParameters(Executable executable) { } @Test - public void nonAnnotatedParametersInTopLevelClassConstructorAreNotCandidatesForAutowiring() throws Exception { + void nonAnnotatedParametersInTopLevelClassConstructorAreNotCandidatesForAutowiring() throws Exception { Constructor notAutowirableConstructor = AutowirableClass.class.getConstructor(String.class); Parameter[] parameters = notAutowirableConstructor.getParameters(); @@ -92,21 +92,21 @@ public void nonAnnotatedParametersInTopLevelClassConstructorAreNotCandidatesForA } @Test - public void resolveDependencyPreconditionsForParameter() { + void resolveDependencyPreconditionsForParameter() { assertThatIllegalArgumentException() .isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, null, mock())) .withMessageContaining("Parameter must not be null"); } @Test - public void resolveDependencyPreconditionsForContainingClass() throws Exception { + void resolveDependencyPreconditionsForContainingClass() { assertThatIllegalArgumentException().isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null)) .withMessageContaining("Containing class must not be null"); } @Test - public void resolveDependencyPreconditionsForBeanFactory() throws Exception { + void resolveDependencyPreconditionsForBeanFactory() { assertThatIllegalArgumentException().isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null)) .withMessageContaining("AutowireCapableBeanFactory must not be null"); @@ -118,7 +118,7 @@ private Parameter getParameter() throws NoSuchMethodException { } @Test - public void resolveDependencyForAnnotatedParametersInTopLevelClassConstructor() throws Exception { + void resolveDependencyForAnnotatedParametersInTopLevelClassConstructor() throws Exception { Constructor constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class); AutowireCapableBeanFactory beanFactory = mock(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AotServicesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AotServicesTests.java index 79909f1c68d4..f617b58e751d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AotServicesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AotServicesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -115,7 +115,7 @@ void iteratorReturnsServicesIterator() { AotServices loaded = AotServices .factories(new TestSpringFactoriesClassLoader("aot-services.factories")) .load(TestService.class); - assertThat(loaded.iterator().next()).isInstanceOf(TestServiceImpl.class); + assertThat(loaded).singleElement().isInstanceOf(TestServiceImpl.class); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java index aae78a040a80..e7c986925ad6 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import javax.lang.model.element.Modifier; import javax.xml.parsers.DocumentBuilderFactory; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GeneratedMethod; @@ -44,6 +45,8 @@ import org.springframework.beans.testfixture.beans.AnnotatedBean; import org.springframework.beans.testfixture.beans.GenericBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.aot.CustomBean; +import org.springframework.beans.testfixture.beans.factory.aot.CustomPropertyValue; import org.springframework.beans.testfixture.beans.factory.aot.InnerBeanConfiguration; import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationsCode; import org.springframework.beans.testfixture.beans.factory.aot.SimpleBean; @@ -52,19 +55,22 @@ import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Implementation; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.One; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Two; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean; import org.springframework.core.ResolvableType; import org.springframework.core.test.io.support.MockSpringFactoriesLoader; import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.Compiled; import org.springframework.core.test.tools.SourceFile; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link BeanDefinitionMethodGenerator} and @@ -155,7 +161,8 @@ void generateWithTargetTypeUsingGenericsSetsBothBeanClassAndTargetType() { @Test void generateWithBeanClassAndFactoryMethodNameSetsTargetTypeAndBeanClass() { - this.beanFactory.registerSingleton("factory", new SimpleBeanConfiguration()); + this.beanFactory.registerBeanDefinition("factory", + new RootBeanDefinition(SimpleBeanConfiguration.class)); RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class); beanDefinition.setFactoryBeanName("factory"); beanDefinition.setFactoryMethodName("simpleBean"); @@ -176,7 +183,8 @@ void generateWithBeanClassAndFactoryMethodNameSetsTargetTypeAndBeanClass() { @Test void generateWithTargetTypeAndFactoryMethodNameSetsOnlyBeanClass() { - this.beanFactory.registerSingleton("factory", new SimpleBeanConfiguration()); + this.beanFactory.registerBeanDefinition("factory", + new RootBeanDefinition(SimpleBeanConfiguration.class)); RootBeanDefinition beanDefinition = new RootBeanDefinition(); beanDefinition.setTargetType(SimpleBean.class); beanDefinition.setFactoryBeanName("factory"); @@ -258,22 +266,6 @@ void generateBeanDefinitionMethodUSeBeanClassNameIfNotReachable() { }); } - @Test // gh-29556 - void generateBeanDefinitionMethodGeneratesMethodWithInstanceSupplier() { - RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class, TestBean::new)); - BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( - this.methodGeneratorFactory, registeredBean, null, - List.of((generationContext, beanRegistrationCode) -> { })); - MethodReference method = generator.generateBeanDefinitionMethod( - this.generationContext, this.beanRegistrationsCode); - compile(method, (actual, compiled) -> { - SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); - assertThat(sourceFile).contains("Get the bean definition for 'testBean'"); - assertThat(sourceFile).contains("setInstanceSupplier(TestBean::new)"); - assertThat(actual).isInstanceOf(RootBeanDefinition.class); - }); - } - @Test void generateBeanDefinitionMethodWhenHasInnerClassTargetMethodGeneratesMethod() { this.beanFactory.registerBeanDefinition("testBeanConfiguration", new RootBeanDefinition( @@ -379,6 +371,7 @@ void generateBeanDefinitionMethodWhenHasInstancePostProcessorGeneratesMethod() { assertThat(instance.getName()).isEqualTo("postprocessed"); } catch (Exception ex) { + throw new IllegalStateException(ex); } SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); assertThat(sourceFile).contains("instanceSupplier.andThen("); @@ -410,13 +403,13 @@ void generateBeanDefinitionMethodWhenHasInstancePostProcessorAndFactoryMethodGen compile(method, (actual, compiled) -> { assertThat(compiled.getSourceFile(".*BeanDefinitions")).contains("BeanInstanceSupplier"); assertThat(actual.getBeanClass()).isEqualTo(TestBean.class); - InstanceSupplier supplier = (InstanceSupplier) actual - .getInstanceSupplier(); + InstanceSupplier supplier = (InstanceSupplier) actual.getInstanceSupplier(); try { TestBean instance = (TestBean) supplier.get(registeredBean); assertThat(instance.getName()).isEqualTo("postprocessed"); } catch (Exception ex) { + throw new IllegalStateException(ex); } SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); assertThat(sourceFile).contains("instanceSupplier.andThen("); @@ -539,8 +532,7 @@ void generateBeanDefinitionMethodWhenHasInnerBeanPropertyValueGeneratesMethod() assertThat(actualInnerBeanDefinition.isPrimary()).isTrue(); assertThat(actualInnerBeanDefinition.getRole()) .isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); - Supplier innerInstanceSupplier = actualInnerBeanDefinition - .getInstanceSupplier(); + Supplier innerInstanceSupplier = actualInnerBeanDefinition.getInstanceSupplier(); try { assertThat(innerInstanceSupplier.get()).isInstanceOf(AnnotatedBean.class); } @@ -605,8 +597,7 @@ void generateBeanDefinitionMethodWhenHasInnerBeanConstructorValueGeneratesMethod assertThat(actualInnerBeanDefinition.isPrimary()).isTrue(); assertThat(actualInnerBeanDefinition.getRole()) .isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); - Supplier innerInstanceSupplier = actualInnerBeanDefinition - .getInstanceSupplier(); + Supplier innerInstanceSupplier = actualInnerBeanDefinition.getInstanceSupplier(); try { assertThat(innerInstanceSupplier.get()).isInstanceOf(String.class); } @@ -618,6 +609,23 @@ void generateBeanDefinitionMethodWhenHasInnerBeanConstructorValueGeneratesMethod }); } + @Test + void generateBeanDefinitionMethodWhenCustomPropertyValueUsesCustomDelegate() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(CustomBean.class); + beanDefinition.getPropertyValues().addPropertyValue( + "customPropertyValue", new CustomPropertyValue("test")); + RegisteredBean bean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, bean, "test", + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> + assertThat(actual.getPropertyValues().get("customPropertyValue")) + .isInstanceOfSatisfying(CustomPropertyValue.class, customPropertyValue + -> assertThat(customPropertyValue.value()).isEqualTo("test"))); + } + @Test void generateBeanDefinitionMethodWhenHasAotContributionsAppliesContributions() { RegisteredBean registeredBean = registerBean( @@ -677,12 +685,121 @@ void generateBeanDefinitionMethodWhenBeanIsOfPrimitiveType() { testBeanDefinitionMethodInCurrentFile(Boolean.class, beanDefinition); } - @Test // gh-29556 - void throwExceptionWithInstanceSupplierWithoutAotContribution() { + @Test + void generateBeanDefinitionMethodWhenInstanceSupplierWithNoCustomization() { + RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class, TestBean::new)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + List.of()); + assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode)).withMessage( + "Error processing bean with name 'testBean': instance supplier is not supported"); + } + + @Test + void generateBeanDefinitionMethodWhenInstanceSupplierWithOnlyCustomTarget() { + BeanRegistrationAotContribution aotContribution = BeanRegistrationAotContribution.withCustomCodeFragments( + defaultCodeFragments -> new BeanRegistrationCodeFragmentsDecorator(defaultCodeFragments) { + @Override + public ClassName getTarget(RegisteredBean registeredBean) { + return ClassName.get(TestBean.class); + } + }); + RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class, TestBean::new)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + List.of(aotContribution)); + assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode)).withMessageStartingWith( + "Default code generation is not supported for bean definitions declaring an instance supplier callback"); + } + + @Test + void generateBeanDefinitionMethodWhenInstanceSupplierWithOnlyCustomInstanceSupplier() { + BeanRegistrationAotContribution aotContribution = BeanRegistrationAotContribution.withCustomCodeFragments( + defaultCodeFragments -> new BeanRegistrationCodeFragmentsDecorator(defaultCodeFragments) { + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + return CodeBlock.of("// custom"); + } + }); + RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class, TestBean::new)); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + List.of(aotContribution)); + assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode)).withMessage( + "Error processing bean with name 'testBean': instance supplier is not supported"); + } + + @Test + void generateBeanDefinitionMethodWhenInstanceSupplierWithCustomInstanceSupplierAndCustomTarget() { + BeanRegistrationAotContribution aotContribution = BeanRegistrationAotContribution.withCustomCodeFragments( + defaultCodeFragments -> new BeanRegistrationCodeFragmentsDecorator(defaultCodeFragments) { + + @Override + public ClassName getTarget(RegisteredBean registeredBean) { + return ClassName.get(TestBean.class); + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + return CodeBlock.of("$T::new", TestBean.class); + } + }); RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class, TestBean::new)); - assertThatIllegalArgumentException().isThrownBy(() -> new BeanDefinitionMethodGenerator( + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( this.methodGeneratorFactory, registeredBean, null, - Collections.emptyList())); + List.of(aotContribution)); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> { + SourceFile sourceFile = compiled.getSourceFile(".*BeanDefinitions"); + assertThat(sourceFile).contains("Get the bean definition for 'testBean'"); + assertThat(sourceFile).contains("setInstanceSupplier(TestBean::new)"); + assertThat(actual).isInstanceOf(RootBeanDefinition.class); + }); + } + + @Nested + @SuppressWarnings("deprecation") + class DeprecationTests { + + private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem() + .withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror"); + + @Test + void generateBeanDefinitionMethodWithDeprecatedTargetClass() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(DeprecatedBean.class); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + generationContext, beanRegistrationsCode); + compileAndCheckWarnings(method); + } + + @Test + void generateBeanDefinitionMethodWithDeprecatedGenericElementInTargetClass() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(GenericBean.class, DeprecatedBean.class)); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + generationContext, beanRegistrationsCode); + compileAndCheckWarnings(method); + } + + private void compileAndCheckWarnings(MethodReference methodReference) { + assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, methodReference, + ((instanceSupplier, compiled) -> {}))); + } + } private void testBeanDefinitionMethodInCurrentFile(Class targetType, RootBeanDefinition beanDefinition) { @@ -709,6 +826,10 @@ private RegisteredBean registerBean(RootBeanDefinition beanDefinition) { } private void compile(MethodReference method, BiConsumer result) { + compile(TestCompiler.forSystem(), method, result); + } + + private void compile(TestCompiler testCompiler, MethodReference method, BiConsumer result) { this.beanRegistrationsCode.getTypeBuilder().set(type -> { CodeBlock methodInvocation = method.toInvokeCodeBlock(ArgumentCodeGenerator.none(), this.beanRegistrationsCode.getClassName()); @@ -720,7 +841,7 @@ private void compile(MethodReference method, BiConsumer + testCompiler.with(this.generationContext).compile(compiled -> result.accept((RootBeanDefinition) compiled.getInstance(Supplier.class).get(), compiled)); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java index 3edeb086c2b3..816a886dd725 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; @@ -28,16 +28,20 @@ import javax.lang.model.element.Modifier; -import org.junit.jupiter.api.BeforeEach; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; +import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.config.RuntimeBeanNameReference; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -47,6 +51,7 @@ import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.support.ManagedSet; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.aot.CustomPropertyValue; import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder; import org.springframework.core.test.tools.Compiled; import org.springframework.core.test.tools.TestCompiler; @@ -217,18 +222,63 @@ void setRoleWhenOther() { } @Test - void constructorArgumentValuesWhenValues() { + void constructorArgumentValuesWhenIndexedValues() { this.beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, String.class); this.beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(1, "test"); this.beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(2, 123); compile((actual, compiled) -> { - Map values = actual.getConstructorArgumentValues().getIndexedArgumentValues(); - assertThat(values.get(0).getValue()).isEqualTo(String.class); - assertThat(values.get(1).getValue()).isEqualTo("test"); - assertThat(values.get(2).getValue()).isEqualTo(123); + ConstructorArgumentValues argumentValues = actual.getConstructorArgumentValues(); + Map values = argumentValues.getIndexedArgumentValues(); + assertThat(values.get(0)).satisfies(assertValueHolder(String.class, null, null)); + assertThat(values.get(1)).satisfies(assertValueHolder("test", null, null)); + assertThat(values.get(2)).satisfies(assertValueHolder(123, null, null)); + assertThat(values).hasSize(3); + assertThat(argumentValues.getGenericArgumentValues()).isEmpty(); }); } + @Test + void constructorArgumentValuesWhenIndexedNullValue() { + this.beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null); + compile((actual, compiled) -> { + ConstructorArgumentValues argumentValues = actual.getConstructorArgumentValues(); + Map values = argumentValues.getIndexedArgumentValues(); + assertThat(values.get(0)).satisfies(assertValueHolder(null, null, null)); + assertThat(values).hasSize(1); + assertThat(argumentValues.getGenericArgumentValues()).isEmpty(); + }); + } + + @Test + void constructorArgumentValuesWhenGenericValuesWithName() { + this.beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(String.class); + this.beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(2, Long.class.getName()); + this.beanDefinition.getConstructorArgumentValues().addGenericArgumentValue( + new ValueHolder("value", null, "param1")); + this.beanDefinition.getConstructorArgumentValues().addGenericArgumentValue( + new ValueHolder("another", CharSequence.class.getName(), "param2")); + compile((actual, compiled) -> { + ConstructorArgumentValues argumentValues = actual.getConstructorArgumentValues(); + List values = argumentValues.getGenericArgumentValues(); + assertThat(values).satisfiesExactly( + assertValueHolder(String.class, null, null), + assertValueHolder(2, Long.class, null), + assertValueHolder("value", null, "param1"), + assertValueHolder("another", CharSequence.class, "param2")); + assertThat(argumentValues.getIndexedArgumentValues()).isEmpty(); + }); + } + + private Consumer assertValueHolder( + @Nullable Object value, @Nullable Class type, @Nullable String name) { + + return valueHolder -> { + assertThat(valueHolder.getValue()).isEqualTo(value); + assertThat(valueHolder.getType()).isEqualTo((type != null ? type.getName() : null)); + assertThat(valueHolder.getName()).isEqualTo(name); + }; + } + @Test void propertyValuesWhenValues() { this.beanDefinition.setTargetType(PropertyValuesBean.class); @@ -239,6 +289,21 @@ void propertyValuesWhenValues() { assertThat(actual.getPropertyValues().get("spring")).isEqualTo("framework"); }); assertHasMethodInvokeHints(PropertyValuesBean.class, "setTest", "setSpring"); + assertHasDeclaredFieldsHint(PropertyValuesBean.class); + } + + @Test + void propertyValuesWhenValuesOnParentClass() { + this.beanDefinition.setTargetType(ExtendedPropertyValuesBean.class); + this.beanDefinition.getPropertyValues().add("test", String.class); + this.beanDefinition.getPropertyValues().add("spring", "framework"); + compile((actual, compiled) -> { + assertThat(actual.getPropertyValues().get("test")).isEqualTo(String.class); + assertThat(actual.getPropertyValues().get("spring")).isEqualTo("framework"); + }); + assertHasMethodInvokeHints(PropertyValuesBean.class, "setTest", "setSpring"); + assertHasDeclaredFieldsHint(ExtendedPropertyValuesBean.class); + assertHasDeclaredFieldsHint(PropertyValuesBean.class); } @Test @@ -248,7 +313,7 @@ void propertyValuesWhenContainsBeanReference() { assertThat(actual.getPropertyValues().contains("myService")).isTrue(); assertThat(actual.getPropertyValues().get("myService")) .isInstanceOfSatisfying(RuntimeBeanReference.class, - beanReference -> assertThat(beanReference.getBeanName()).isEqualTo("test")); + beanReference -> assertThat(beanReference.getBeanName()).isEqualTo("test")); }); } @@ -259,8 +324,8 @@ void propertyValuesWhenContainsManagedList() { this.beanDefinition.getPropertyValues().add("value", managedList); compile((actual, compiled) -> { Object value = actual.getPropertyValues().get("value"); - assertThat(value).isInstanceOf(ManagedList.class); - assertThat(((List) value).get(0)).isInstanceOf(BeanReference.class); + assertThat(value).isInstanceOf(ManagedList.class).asInstanceOf(InstanceOfAssertFactories.LIST) + .singleElement().isInstanceOf(BeanReference.class); }); } @@ -271,8 +336,9 @@ void propertyValuesWhenContainsManagedSet() { this.beanDefinition.getPropertyValues().add("value", managedSet); compile((actual, compiled) -> { Object value = actual.getPropertyValues().get("value"); - assertThat(value).isInstanceOf(ManagedSet.class); - assertThat(((Set) value).iterator().next()).isInstanceOf(BeanReference.class); + assertThat(value).isInstanceOf(ManagedSet.class) + .asInstanceOf(InstanceOfAssertFactories.COLLECTION) + .singleElement().isInstanceOf(BeanReference.class); }); } @@ -283,8 +349,9 @@ void propertyValuesWhenContainsManagedMap() { this.beanDefinition.getPropertyValues().add("value", managedMap); compile((actual, compiled) -> { Object value = actual.getPropertyValues().get("value"); - assertThat(value).isInstanceOf(ManagedMap.class); - assertThat(((Map) value).get("test")).isInstanceOf(BeanReference.class); + assertThat(value).isInstanceOf(ManagedMap.class) + .asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class)) + .hasEntrySatisfying("test", ref -> assertThat(ref).isInstanceOf(BeanReference.class)); }); } @@ -298,7 +365,23 @@ void propertyValuesWhenValuesOnFactoryBeanClass() { assertThat(actual.getPropertyValues().get("prefix")).isEqualTo("Hello"); assertThat(actual.getPropertyValues().get("name")).isEqualTo("World"); }); - assertHasMethodInvokeHints(PropertyValuesFactoryBean.class, "setPrefix", "setName" ); + assertHasMethodInvokeHints(PropertyValuesFactoryBean.class, "setPrefix", "setName"); + assertHasDeclaredFieldsHint(PropertyValuesFactoryBean.class); + } + + @Test + void propertyValuesWhenCustomValuesUsingDelegate() { + this.beanDefinition.setTargetType(PropertyValuesBean.class); + this.beanDefinition.getPropertyValues().add("test", new CustomPropertyValue("test")); + this.beanDefinition.getPropertyValues().add("spring", new CustomPropertyValue("framework")); + compile(value -> true, List.of(new CustomPropertyValue.ValueCodeGeneratorDelegate()), (actual, compiled) -> { + assertThat(actual.getPropertyValues().get("test")).isInstanceOfSatisfying(CustomPropertyValue.class, + customPropertyValue -> assertThat(customPropertyValue.value()).isEqualTo("test")); + assertThat(actual.getPropertyValues().get("spring")).isInstanceOfSatisfying(CustomPropertyValue.class, + customPropertyValue -> assertThat(customPropertyValue.value()).isEqualTo("framework")); + }); + assertHasMethodInvokeHints(PropertyValuesBean.class, "setTest", "setSpring"); + assertHasDeclaredFieldsHint(PropertyValuesBean.class); } @Test @@ -348,9 +431,8 @@ void qualifiersWhenMultipleQualifiers() { this.beanDefinition.addQualifier(new AutowireCandidateQualifier("com.example.Another", ChronoUnit.SECONDS)); compile((actual, compiled) -> { List qualifiers = new ArrayList<>(actual.getQualifiers()); - assertThat(qualifiers.get(0)).satisfies(isQualifierFor("com.example.Qualifier", "id")); - assertThat(qualifiers.get(1)).satisfies(isQualifierFor("com.example.Another", ChronoUnit.SECONDS)); - assertThat(qualifiers).hasSize(2); + assertThat(qualifiers).satisfiesExactly(isQualifierFor("com.example.Qualifier", "id"), + isQualifierFor("com.example.Another", ChronoUnit.SECONDS)); }); } @@ -376,28 +458,36 @@ void multipleItems() { @Nested class InitDestroyMethodTests { - private final String privateInitMethod = InitDestroyBean.class.getName() + ".privateInit"; - private final String privateDestroyMethod = InitDestroyBean.class.getName() + ".privateDestroy"; + private static final String privateInitMethod = InitDestroyBean.class.getName() + ".privateInit"; - @BeforeEach - void setTargetType() { - beanDefinition.setTargetType(InitDestroyBean.class); - } + private static final String privateDestroyMethod = InitDestroyBean.class.getName() + ".privateDestroy"; @Test void noInitMethod() { + beanDefinition.setTargetType(InitDestroyBean.class); compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).isNull()); } @Test void singleInitMethod() { + beanDefinition.setTargetType(InitDestroyBean.class); beanDefinition.setInitMethodName("init"); compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).containsExactly("init")); assertHasMethodInvokeHints(InitDestroyBean.class, "init"); } + @Test + void singleInitMethodFromInterface() { + beanDefinition.setTargetType(InitializableTestBean.class); + beanDefinition.setInitMethodName("initialize"); + compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).containsExactly("initialize")); + assertHasMethodInvokeHints(InitializableTestBean.class, "initialize"); + assertHasMethodInvokeHints(Initializable.class, "initialize"); + } + @Test void privateInitMethod() { + beanDefinition.setTargetType(InitDestroyBean.class); beanDefinition.setInitMethodName(privateInitMethod); compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).containsExactly(privateInitMethod)); assertHasMethodInvokeHints(InitDestroyBean.class, "privateInit"); @@ -405,6 +495,7 @@ void privateInitMethod() { @Test void multipleInitMethods() { + beanDefinition.setTargetType(InitDestroyBean.class); beanDefinition.setInitMethodNames("init", privateInitMethod); compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).containsExactly("init", privateInitMethod)); assertHasMethodInvokeHints(InitDestroyBean.class, "init", "privateInit"); @@ -412,48 +503,82 @@ void multipleInitMethods() { @Test void noDestroyMethod() { + beanDefinition.setTargetType(InitDestroyBean.class); compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).isNull()); + assertReflectionOnPublisher(); } @Test void singleDestroyMethod() { + beanDefinition.setTargetType(InitDestroyBean.class); beanDefinition.setDestroyMethodName("destroy"); compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).containsExactly("destroy")); assertHasMethodInvokeHints(InitDestroyBean.class, "destroy"); + assertReflectionOnPublisher(); + } + + @Test + void singleDestroyMethodFromInterface() { + beanDefinition.setTargetType(DisposableTestBean.class); + beanDefinition.setDestroyMethodName("dispose"); + compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).containsExactly("dispose")); + assertHasMethodInvokeHints(DisposableTestBean.class, "dispose"); + assertHasMethodInvokeHints(Disposable.class, "dispose"); + assertReflectionOnPublisher(); } @Test void privateDestroyMethod() { + beanDefinition.setTargetType(InitDestroyBean.class); beanDefinition.setDestroyMethodName(privateDestroyMethod); compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).containsExactly(privateDestroyMethod)); assertHasMethodInvokeHints(InitDestroyBean.class, "privateDestroy"); + assertReflectionOnPublisher(); } @Test void multipleDestroyMethods() { + beanDefinition.setTargetType(InitDestroyBean.class); beanDefinition.setDestroyMethodNames("destroy", privateDestroyMethod); compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).containsExactly("destroy", privateDestroyMethod)); assertHasMethodInvokeHints(InitDestroyBean.class, "destroy", "privateDestroy"); + assertReflectionOnPublisher(); + } + + private void assertReflectionOnPublisher() { + assertThat(RuntimeHintsPredicates.reflection().onType(Publisher.class)).accepts(generationContext.getRuntimeHints()); } } private void assertHasMethodInvokeHints(Class beanType, String... methodNames) { assertThat(methodNames).allMatch(methodName -> RuntimeHintsPredicates.reflection() - .onMethod(beanType, methodName).invoke() - .test(this.generationContext.getRuntimeHints())); + .onMethod(beanType, methodName).invoke() + .test(this.generationContext.getRuntimeHints())); + } + + private void assertHasDeclaredFieldsHint(Class beanType) { + assertThat(RuntimeHintsPredicates.reflection() + .onType(beanType).withMemberCategory(MemberCategory.DECLARED_FIELDS)) + .accepts(this.generationContext.getRuntimeHints()); } private void compile(BiConsumer result) { compile(attribute -> true, result); } - private void compile(Predicate attributeFilter, BiConsumer result) { + private void compile(Predicate attributeFilter, + BiConsumer result) { + compile(attributeFilter, Collections.emptyList(), result); + } + + private void compile(Predicate attributeFilter, List additionalDelegates, + BiConsumer result) { DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); GeneratedClass generatedClass = this.generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); BeanDefinitionPropertiesCodeGenerator codeGenerator = new BeanDefinitionPropertiesCodeGenerator( this.generationContext.getRuntimeHints(), attributeFilter, - generatedClass.getMethods(), (name, value) -> null); + generatedClass.getMethods(), additionalDelegates, (name, value) -> null); CodeBlock generatedCode = codeGenerator.generateCode(this.beanDefinition); typeBuilder.set(type -> { type.addModifiers(Modifier.PUBLIC); @@ -491,6 +616,31 @@ private void privateDestroy() { } + interface Initializable { + + void initialize(); + } + + static class InitializableTestBean implements Initializable { + + @Override + public void initialize() { + } + } + + interface Disposable { + + void dispose(); + } + + static class DisposableTestBean implements Disposable { + + @Override + public void dispose() { + } + + } + static class PropertyValuesBean { private Class test; @@ -515,6 +665,10 @@ public void setSpring(String spring) { } + static class ExtendedPropertyValuesBean extends PropertyValuesBean { + + } + static class PropertyValuesFactoryBean implements FactoryBean { private String prefix; @@ -539,7 +693,7 @@ public void setName(String name) { @Nullable @Override - public String getObject() throws Exception { + public String getObject() { return getPrefix() + " " + getName(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java similarity index 61% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java rename to spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java index 47b8237359c5..0dafc56c1a23 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.aot.generate.ValueCodeGeneratorDelegates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanNameReference; @@ -47,33 +49,38 @@ import org.springframework.core.ResolvableType; import org.springframework.core.test.tools.Compiled; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.core.testfixture.aot.generate.value.EnumWithClassBody; +import org.springframework.core.testfixture.aot.generate.value.ExampleClass; +import org.springframework.core.testfixture.aot.generate.value.ExampleClass$$GeneratedBy; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Tests for {@link BeanDefinitionPropertyValueCodeGenerator}. + * Tests for {@link BeanDefinitionPropertyValueCodeGeneratorDelegates}. This + * also tests that code generated by {@link ValueCodeGeneratorDelegates} + * compiles. * * @author Stephane Nicoll * @author Phillip Webb * @author Sebastien Deleuze * @since 6.0 - * @see BeanDefinitionPropertyValueCodeGeneratorTests */ -class BeanDefinitionPropertyValueCodeGeneratorTests { +class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests { - private static BeanDefinitionPropertyValueCodeGenerator createPropertyValuesCodeGenerator(GeneratedClass generatedClass) { - return new BeanDefinitionPropertyValueCodeGenerator(generatedClass.getMethods(), null); + private static ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) { + return ValueCodeGenerator.with(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES) + .add(ValueCodeGeneratorDelegates.INSTANCES) + .scoped(generatedClass.getMethods()); } private void compile(Object value, BiConsumer result) { TestGenerationContext generationContext = new TestGenerationContext(); DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); - CodeBlock generatedCode = createPropertyValuesCodeGenerator(generatedClass).generateCode(value); + CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value); typeBuilder.set(type -> { type.addModifiers(Modifier.PUBLIC); type.addSuperinterface( @@ -101,90 +108,72 @@ class PrimitiveTests { @Test void generateWhenBoolean() { - compile(true, (instance, compiled) -> { - assertThat(instance).isEqualTo(Boolean.TRUE); - assertThat(compiled.getSourceFile()).contains("true"); - }); + compile(true, (instance, compiled) -> + assertThat(instance).isEqualTo(Boolean.TRUE)); } @Test void generateWhenByte() { - compile((byte) 2, (instance, compiled) -> { - assertThat(instance).isEqualTo((byte) 2); - assertThat(compiled.getSourceFile()).contains("(byte) 2"); - }); + compile((byte) 2, (instance, compiled) -> + assertThat(instance).isEqualTo((byte) 2)); } @Test void generateWhenShort() { - compile((short) 3, (instance, compiled) -> { - assertThat(instance).isEqualTo((short) 3); - assertThat(compiled.getSourceFile()).contains("(short) 3"); - }); + compile((short) 3, (instance, compiled) -> + assertThat(instance).isEqualTo((short) 3)); } @Test void generateWhenInt() { - compile(4, (instance, compiled) -> { - assertThat(instance).isEqualTo(4); - assertThat(compiled.getSourceFile()).contains("return 4;"); - }); + compile(4, (instance, compiled) -> + assertThat(instance).isEqualTo(4)); } @Test void generateWhenLong() { - compile(5L, (instance, compiled) -> { - assertThat(instance).isEqualTo(5L); - assertThat(compiled.getSourceFile()).contains("5L"); - }); + compile(5L, (instance, compiled) -> + assertThat(instance).isEqualTo(5L)); } @Test void generateWhenFloat() { - compile(0.1F, (instance, compiled) -> { - assertThat(instance).isEqualTo(0.1F); - assertThat(compiled.getSourceFile()).contains("0.1F"); - }); + compile(0.1F, (instance, compiled) -> + assertThat(instance).isEqualTo(0.1F)); } @Test void generateWhenDouble() { - compile(0.2, (instance, compiled) -> { - assertThat(instance).isEqualTo(0.2); - assertThat(compiled.getSourceFile()).contains("(double) 0.2"); - }); + compile(0.2, (instance, compiled) -> + assertThat(instance).isEqualTo(0.2)); } @Test void generateWhenChar() { - compile('a', (instance, compiled) -> { - assertThat(instance).isEqualTo('a'); - assertThat(compiled.getSourceFile()).contains("'a'"); - }); + compile('a', (instance, compiled) -> + assertThat(instance).isEqualTo('a')); } @Test void generateWhenSimpleEscapedCharReturnsEscaped() { - testEscaped('\b', "'\\b'"); - testEscaped('\t', "'\\t'"); - testEscaped('\n', "'\\n'"); - testEscaped('\f', "'\\f'"); - testEscaped('\r', "'\\r'"); - testEscaped('\"', "'\"'"); - testEscaped('\'', "'\\''"); - testEscaped('\\', "'\\\\'"); + testEscaped('\b'); + testEscaped('\t'); + testEscaped('\n'); + testEscaped('\f'); + testEscaped('\r'); + testEscaped('\"'); + testEscaped('\''); + testEscaped('\\'); } @Test void generatedWhenUnicodeEscapedCharReturnsEscaped() { - testEscaped('\u007f', "'\\u007f'"); + testEscaped('\u007f'); } - private void testEscaped(char value, String expectedSourceContent) { - compile(value, (instance, compiled) -> { - assertThat(instance).isEqualTo(value); - assertThat(compiled.getSourceFile()).contains(expectedSourceContent); - }); + private void testEscaped(char value) { + compile(value, (instance, compiled) -> + assertThat(instance).isEqualTo(value)); } } @@ -194,10 +183,8 @@ class StringTests { @Test void generateWhenString() { - compile("test\n", (instance, compiled) -> { - assertThat(instance).isEqualTo("test\n"); - assertThat(compiled.getSourceFile()).contains("\n"); - }); + compile("test\n", (instance, compiled) -> + assertThat(instance).isEqualTo("test\n")); } } @@ -207,10 +194,8 @@ class CharsetTests { @Test void generateWhenCharset() { - compile(StandardCharsets.UTF_8, (instance, compiled) -> { - assertThat(instance).isEqualTo(Charset.forName("UTF-8")); - assertThat(compiled.getSourceFile()).contains("\"UTF-8\""); - }); + compile(StandardCharsets.UTF_8, (instance, compiled) -> + assertThat(instance).isEqualTo(Charset.forName("UTF-8"))); } } @@ -220,18 +205,14 @@ class EnumTests { @Test void generateWhenEnum() { - compile(ChronoUnit.DAYS, (instance, compiled) -> { - assertThat(instance).isEqualTo(ChronoUnit.DAYS); - assertThat(compiled.getSourceFile()).contains("ChronoUnit.DAYS"); - }); + compile(ChronoUnit.DAYS, (instance, compiled) -> + assertThat(instance).isEqualTo(ChronoUnit.DAYS)); } @Test void generateWhenEnumWithClassBody() { - compile(EnumWithClassBody.TWO, (instance, compiled) -> { - assertThat(instance).isEqualTo(EnumWithClassBody.TWO); - assertThat(compiled.getSourceFile()).contains("EnumWithClassBody.TWO"); - }); + compile(EnumWithClassBody.TWO, (instance, compiled) -> + assertThat(instance).isEqualTo(EnumWithClassBody.TWO)); } } @@ -266,18 +247,16 @@ void generateWhenSimpleResolvableType() { @Test void generateWhenNoneResolvableType() { ResolvableType resolvableType = ResolvableType.NONE; - compile(resolvableType, (instance, compiled) -> { - assertThat(instance).isEqualTo(resolvableType); - assertThat(compiled.getSourceFile()).contains("ResolvableType.NONE"); - }); + compile(resolvableType, (instance, compiled) -> + assertThat(instance).isEqualTo(resolvableType)); } @Test void generateWhenGenericResolvableType() { ResolvableType resolvableType = ResolvableType .forClassWithGenerics(List.class, String.class); - compile(resolvableType, (instance, compiled) -> assertThat(instance) - .isEqualTo(resolvableType)); + compile(resolvableType, (instance, compiled) -> + assertThat(instance).isEqualTo(resolvableType)); } @Test @@ -298,28 +277,22 @@ class ArrayTests { @Test void generateWhenPrimitiveArray() { byte[] bytes = { 0, 1, 2 }; - compile(bytes, (instance, compiler) -> { - assertThat(instance).isEqualTo(bytes); - assertThat(compiler.getSourceFile()).contains("new byte[]"); - }); + compile(bytes, (instance, compiler) -> + assertThat(instance).isEqualTo(bytes)); } @Test void generateWhenWrapperArray() { Byte[] bytes = { 0, 1, 2 }; - compile(bytes, (instance, compiler) -> { - assertThat(instance).isEqualTo(bytes); - assertThat(compiler.getSourceFile()).contains("new Byte[]"); - }); + compile(bytes, (instance, compiler) -> + assertThat(instance).isEqualTo(bytes)); } @Test void generateWhenClassArray() { Class[] classes = new Class[] { InputStream.class, OutputStream.class }; - compile(classes, (instance, compiler) -> { - assertThat(instance).isEqualTo(classes); - assertThat(compiler.getSourceFile()).contains("new Class[]"); - }); + compile(classes, (instance, compiler) -> + assertThat(instance).isEqualTo(classes)); } } @@ -402,10 +375,7 @@ void generateWhenStringList() { @Test void generateWhenEmptyList() { List list = List.of(); - compile(list, (instance, compiler) -> { - assertThat(instance).isEqualTo(list); - assertThat(compiler.getSourceFile()).contains("Collections.emptyList();"); - }); + compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list)); } } @@ -423,20 +393,14 @@ void generateWhenStringSet() { @Test void generateWhenEmptySet() { Set set = Set.of(); - compile(set, (instance, compiler) -> { - assertThat(instance).isEqualTo(set); - assertThat(compiler.getSourceFile()).contains("Collections.emptySet();"); - }); + compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set)); } @Test void generateWhenLinkedHashSet() { Set set = new LinkedHashSet<>(List.of("a", "b", "c")); - compile(set, (instance, compiler) -> { - assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class); - assertThat(compiler.getSourceFile()) - .contains("new LinkedHashSet(List.of("); - }); + compile(set, (instance, compiler) -> + assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class)); } @Test @@ -453,10 +417,8 @@ class MapTests { @Test void generateWhenSmallMap() { Map map = Map.of("k1", "v1", "k2", "v2"); - compile(map, (instance, compiler) -> { - assertThat(instance).isEqualTo(map); - assertThat(compiler.getSourceFile()).contains("Map.of("); - }); + compile(map, (instance, compiler) -> + assertThat(instance).isEqualTo(map)); } @Test @@ -465,10 +427,7 @@ void generateWhenMapWithOverTenElements() { for (int i = 1; i <= 11; i++) { map.put("k" + i, "v" + i); } - compile(map, (instance, compiler) -> { - assertThat(instance).isEqualTo(map); - assertThat(compiler.getSourceFile()).contains("Map.ofEntries("); - }); + compile(map, (instance, compiler) -> assertThat(instance).isEqualTo(map)); } @Test @@ -518,47 +477,4 @@ void generatedWhenBeanReferenceByType() { } - @Nested - static class ExceptionTests { - - @Test - void generateWhenUnsupportedDataTypeThrowsException() { - SampleValue sampleValue = new SampleValue("one"); - assertThatIllegalArgumentException().isThrownBy(() -> generateCode(sampleValue)) - .withMessageContaining("Failed to generate code for") - .withMessageContaining(sampleValue.toString()) - .withMessageContaining(SampleValue.class.getName()) - .havingCause() - .withMessageContaining("Code generation does not support") - .withMessageContaining(SampleValue.class.getName()); - } - - @Test - void generateWhenListOfUnsupportedElement() { - SampleValue one = new SampleValue("one"); - SampleValue two = new SampleValue("two"); - List list = List.of(one, two); - assertThatIllegalArgumentException().isThrownBy(() -> generateCode(list)) - .withMessageContaining("Failed to generate code for") - .withMessageContaining(list.toString()) - .withMessageContaining(list.getClass().getName()) - .havingCause() - .withMessageContaining("Failed to generate code for") - .withMessageContaining(one.toString()) - .withMessageContaining("?") - .havingCause() - .withMessageContaining("Code generation does not support ?"); - } - - private void generateCode(Object value) { - TestGenerationContext context = new TestGenerationContext(); - GeneratedClass generatedClass = context.getGeneratedClasses() - .addForFeature("Test", type -> {}); - createPropertyValuesCodeGenerator(generatedClass).generateCode(value); - } - - record SampleValue(String name) {} - - } - } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java index 422de9d4420d..1a1a8f6d56fe 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Stream; @@ -51,6 +52,7 @@ 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.env.Environment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; @@ -292,6 +294,33 @@ void getNestedWithNoGeneratorUsesReflection(Source source) throws Exception { assertThat(instance).isEqualTo("1"); } + @Test // gh-33180 + void getWithNestedInvocationRetainsFactoryMethod() throws Exception { + AtomicReference testMethodReference = new AtomicReference<>(); + AtomicReference anotherMethodReference = new AtomicReference<>(); + + BeanInstanceSupplier nestedInstanceSupplier = BeanInstanceSupplier + .forFactoryMethod(AnotherTestStringFactory.class, "another") + .withGenerator(registeredBean -> { + anotherMethodReference.set(SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod()); + return "Another"; + }); + RegisteredBean nestedRegisteredBean = new Source(String.class, nestedInstanceSupplier).registerBean(this.beanFactory); + BeanInstanceSupplier instanceSupplier = BeanInstanceSupplier + .forFactoryMethod(TestStringFactory.class, "test") + .withGenerator(registeredBean -> { + Object nested = nestedInstanceSupplier.get(nestedRegisteredBean); + testMethodReference.set(SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod()); + return "custom" + nested; + }); + RegisteredBean registeredBean = new Source(String.class, instanceSupplier).registerBean(this.beanFactory); + Object value = instanceSupplier.get(registeredBean); + + assertThat(value).isEqualTo("customAnother"); + assertThat(testMethodReference.get()).isEqualTo(instanceSupplier.getFactoryMethod()); + assertThat(anotherMethodReference.get()).isEqualTo(nestedInstanceSupplier.getFactoryMethod()); + } + @Test void resolveArgumentsWithNoArgConstructor() { RootBeanDefinition beanDefinition = new RootBeanDefinition( @@ -444,7 +473,7 @@ void resolveArgumentsWithMultiArgsConstructor(Source source) { } @ParameterizedResolverTest(Sources.MIXED_ARGS) - void resolveArgumentsWithMixedArgsConstructorWithUserValue(Source source) { + void resolveArgumentsWithMixedArgsConstructorWithIndexedUserValue(Source source) { ResourceLoader resourceLoader = new DefaultResourceLoader(); Environment environment = mock(); this.beanFactory.registerResolvableDependency(ResourceLoader.class, @@ -465,7 +494,28 @@ void resolveArgumentsWithMixedArgsConstructorWithUserValue(Source source) { } @ParameterizedResolverTest(Sources.MIXED_ARGS) - void resolveArgumentsWithMixedArgsConstructorWithUserBeanReference(Source source) { + void resolveArgumentsWithMixedArgsConstructorWithGenericUserValue(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addGenericArgumentValue("user-value"); + }); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo("user-value"); + assertThat(arguments.getObject(2)).isEqualTo(environment); + } + + @ParameterizedResolverTest(Sources.MIXED_ARGS) + void resolveArgumentsWithMixedArgsConstructorAndIndexedUserBeanReference(Source source) { ResourceLoader resourceLoader = new DefaultResourceLoader(); Environment environment = mock(); this.beanFactory.registerResolvableDependency(ResourceLoader.class, @@ -487,8 +537,31 @@ void resolveArgumentsWithMixedArgsConstructorWithUserBeanReference(Source source assertThat(arguments.getObject(2)).isEqualTo(environment); } + @ParameterizedResolverTest(Sources.MIXED_ARGS) + void resolveArgumentsWithMixedArgsConstructorAndGenericUserBeanReference(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addGenericArgumentValue(new RuntimeBeanReference("two")); + }); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo("2"); + assertThat(arguments.getObject(2)).isEqualTo(environment); + } + @Test - void resolveArgumentsWithUserValueWithTypeConversionRequired() { + void resolveIndexedArgumentsWithUserValueWithTypeConversionRequired() { Source source = new Source(CharDependency.class, BeanInstanceSupplier.forConstructor(char.class)); RegisteredBean registerBean = source.registerBean(this.beanFactory, @@ -503,8 +576,24 @@ void resolveArgumentsWithUserValueWithTypeConversionRequired() { assertThat(arguments.getObject(0)).isInstanceOf(Character.class).isEqualTo('\\'); } + @Test + void resolveGenericArgumentsWithUserValueWithTypeConversionRequired() { + Source source = new Source(CharDependency.class, + BeanInstanceSupplier.forConstructor(char.class)); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addGenericArgumentValue("\\", char.class.getName()); + }); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isInstanceOf(Character.class).isEqualTo('\\'); + } + @ParameterizedResolverTest(Sources.SINGLE_ARG) - void resolveArgumentsWithUserValueWithBeanReference(Source source) { + void resolveIndexedArgumentsWithUserValueWithBeanReference(Source source) { this.beanFactory.registerSingleton("stringBean", "string"); RegisteredBean registerBean = source.registerBean(this.beanFactory, beanDefinition -> beanDefinition.getConstructorArgumentValues() @@ -516,7 +605,18 @@ void resolveArgumentsWithUserValueWithBeanReference(Source source) { } @ParameterizedResolverTest(Sources.SINGLE_ARG) - void resolveArgumentsWithUserValueWithBeanDefinition(Source source) { + void resolveGenericArgumentsWithUserValueWithBeanReference(Source source) { + this.beanFactory.registerSingleton("stringBean", "string"); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> beanDefinition.getConstructorArgumentValues() + .addGenericArgumentValue(new RuntimeBeanReference("stringBean"))); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("string"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveIndexedArgumentsWithUserValueWithBeanDefinition(Source source) { AbstractBeanDefinition userValue = BeanDefinitionBuilder .rootBeanDefinition(String.class, () -> "string").getBeanDefinition(); RegisteredBean registerBean = source.registerBean(this.beanFactory, @@ -528,11 +628,23 @@ void resolveArgumentsWithUserValueWithBeanDefinition(Source source) { } @ParameterizedResolverTest(Sources.SINGLE_ARG) - void resolveArgumentsWithUserValueThatIsAlreadyResolved(Source source) { + void resolveGenericArgumentsWithUserValueWithBeanDefinition(Source source) { + AbstractBeanDefinition userValue = BeanDefinitionBuilder + .rootBeanDefinition(String.class, () -> "string").getBeanDefinition(); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> beanDefinition.getConstructorArgumentValues() + .addGenericArgumentValue(userValue)); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("string"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveIndexedArgumentsWithUserValueThatIsAlreadyResolved(Source source) { RegisteredBean registerBean = source.registerBean(this.beanFactory); BeanDefinition mergedBeanDefinition = this.beanFactory .getMergedBeanDefinition("testBean"); - ValueHolder valueHolder = new ValueHolder('a'); + ValueHolder valueHolder = new ValueHolder("a"); valueHolder.setConvertedValue("this is an a"); mergedBeanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, valueHolder); @@ -541,6 +653,19 @@ void resolveArgumentsWithUserValueThatIsAlreadyResolved(Source source) { assertThat(arguments.getObject(0)).isEqualTo("this is an a"); } + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveGenericArgumentsWithUserValueThatIsAlreadyResolved(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + BeanDefinition mergedBeanDefinition = this.beanFactory + .getMergedBeanDefinition("testBean"); + ValueHolder valueHolder = new ValueHolder("a"); + valueHolder.setConvertedValue("this is an a"); + mergedBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(valueHolder); + AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("this is an a"); + } + @Test void resolveArgumentsWhenUsingShortcutsInjectsDirectly() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory() { @@ -934,4 +1059,18 @@ static class MethodOnInterfaceImpl implements MethodOnInterface { } + static class TestStringFactory { + + String test() { + return "test"; + } + } + + static class AnotherTestStringFactory { + + String another() { + return "another"; + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationAotContributionTests.java new file mode 100644 index 000000000000..8f85becbe8c7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationAotContributionTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.aot; + +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationCode; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link BeanRegistrationAotContribution}. + * + * @author Stephane Nicoll + */ +class BeanRegistrationAotContributionTests { + + @Test + void concatWithBothNullReturnsNull() { + assertThat(BeanRegistrationAotContribution.concat(null, null)).isNull(); + } + + @Test + void concatWithFirstNullReturnsSecondAsIs() { + BeanRegistrationAotContribution contribution = mock(BeanRegistrationAotContribution.class); + assertThat(BeanRegistrationAotContribution.concat(null, contribution)).isSameAs(contribution); + verifyNoInteractions(contribution); + } + + @Test + void concatWithSecondNullReturnsFirstAsIs() { + BeanRegistrationAotContribution contribution = mock(BeanRegistrationAotContribution.class); + assertThat(BeanRegistrationAotContribution.concat(contribution, null)).isSameAs(contribution); + verifyNoInteractions(contribution); + } + + @Test + void concatApplyContributionsInOrder() { + BeanRegistrationAotContribution first = mock(BeanRegistrationAotContribution.class); + BeanRegistrationAotContribution second = mock(BeanRegistrationAotContribution.class); + BeanRegistrationAotContribution combined = BeanRegistrationAotContribution.concat(first, second); + assertThat(combined).isNotNull(); + TestGenerationContext generationContext = new TestGenerationContext(); + BeanRegistrationCode beanRegistrationCode = new MockBeanRegistrationCode(generationContext); + combined.applyTo(generationContext, beanRegistrationCode); + InOrder ordered = inOrder(first, second); + ordered.verify(first).applyTo(generationContext, beanRegistrationCode); + ordered.verify(second).applyTo(generationContext, beanRegistrationCode); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java index 833611e825db..ed7b0714bb66 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java @@ -36,9 +36,9 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.beans.testfixture.beans.GenericBeanWithBounds; -import org.springframework.beans.testfixture.beans.Person; -import org.springframework.beans.testfixture.beans.RecordBean; +import org.springframework.beans.testfixture.beans.AgeHolder; +import org.springframework.beans.testfixture.beans.Employee; +import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.factory.aot.MockBeanFactoryInitializationCode; import org.springframework.core.test.io.support.MockSpringFactoriesLoader; @@ -141,36 +141,20 @@ MethodReference generateBeanDefinitionMethod(GenerationContext generationContext @Test void applyToRegisterReflectionHints() { - RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class)); + RegisteredBean registeredBean = registerBean(new RootBeanDefinition(Employee.class)); BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory, registeredBean, null, List.of()); - BeanRegistrationsAotContribution contribution = createContribution(TestBean.class, generator); + BeanRegistrationsAotContribution contribution = createContribution(Employee.class, generator); contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); - assertThat(reflection().onType(TestBean.class) - .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS)) + assertThat(reflection().onType(Employee.class) + .withMemberCategories(MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS)) .accepts(this.generationContext.getRuntimeHints()); - } - - @Test - void applyToRegisterReflectionHintsOnRecordBean() { - RegisteredBean registeredBean = registerBean(new RootBeanDefinition(RecordBean.class)); - BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory, - registeredBean, null, List.of()); - BeanRegistrationsAotContribution contribution = createContribution(RecordBean.class, generator); - contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); - assertThat(reflection().onType(RecordBean.class) - .withMemberCategories(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS)) + assertThat(reflection().onType(ITestBean.class) + .withMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS)) + .accepts(this.generationContext.getRuntimeHints()); + assertThat(reflection().onType(AgeHolder.class) + .withMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS)) .accepts(this.generationContext.getRuntimeHints()); - } - - @Test - void applyToRegisterReflectionHintsOnGenericBeanWithBounds() { - RegisteredBean registeredBean = registerBean(new RootBeanDefinition(GenericBeanWithBounds.class)); - BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory, - registeredBean, null, List.of()); - BeanRegistrationsAotContribution contribution = createContribution(GenericBeanWithBounds.class, generator); - contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); - assertThat(reflection().onType(Person[].class)).accepts(this.generationContext.getRuntimeHints()); } private RegisteredBean registerBean(RootBeanDefinition rootBeanDefinition) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/CodeWarningsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/CodeWarningsTests.java new file mode 100644 index 000000000000..3767c91581c7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/CodeWarningsTests.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.aot; + +import java.util.function.Consumer; +import java.util.stream.Stream; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.testfixture.beans.GenericBean; +import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalBean; +import org.springframework.core.ResolvableType; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.FieldSpec; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.MethodSpec.Builder; +import org.springframework.javapoet.TypeSpec; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CodeWarnings}. + * + * @author Stephane Nicoll + */ +class CodeWarningsTests { + + private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem() + .withCompilerOptions("-Xlint:all", "-Werror"); + + private final CodeWarnings codeWarnings; + + private final TestGenerationContext generationContext; + + CodeWarningsTests() { + this.codeWarnings = new CodeWarnings(); + this.generationContext = new TestGenerationContext(); + } + + @Test + void registerNoWarningDoesNotIncludeAnnotationOnMethod() { + compileWithMethod(method -> { + this.codeWarnings.suppress(method); + method.addStatement("$T bean = $S", String.class, "Hello"); + }, compiled -> assertThat(compiled.getSourceFile()).doesNotContain("@SuppressWarnings")); + } + + @Test + void registerNoWarningDoesNotIncludeAnnotationOnType() { + compile(type -> { + this.codeWarnings.suppress(type); + type.addField(FieldSpec.builder(String.class, "type").build()); + }, compiled -> assertThat(compiled.getSourceFile()).doesNotContain("@SuppressWarnings")); + } + + @Test + @SuppressWarnings("deprecation") + void registerWarningSuppressesItOnMethod() { + this.codeWarnings.register("deprecation"); + compileWithMethod(method -> { + this.codeWarnings.suppress(method); + method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class); + }, compiled -> assertThat(compiled.getSourceFile()) + .contains("@SuppressWarnings(\"deprecation\")")); + } + + @Test + @SuppressWarnings("deprecation") + void registerWarningSuppressesItOnType() { + this.codeWarnings.register("deprecation"); + compile(type -> { + this.codeWarnings.suppress(type); + type.addField(FieldSpec.builder(DeprecatedBean.class, "bean").build()); + }, compiled -> assertThat(compiled.getSourceFile()) + .contains("@SuppressWarnings(\"deprecation\")")); + } + + @Test + @SuppressWarnings({ "deprecation", "removal" }) + void registerSeveralWarningsSuppressesThemOnMethod() { + this.codeWarnings.register("deprecation"); + this.codeWarnings.register("removal"); + compileWithMethod(method -> { + this.codeWarnings.suppress(method); + method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class); + method.addStatement("$T another = new $T()", DeprecatedForRemovalBean.class, DeprecatedForRemovalBean.class); + }, compiled -> assertThat(compiled.getSourceFile()) + .contains("@SuppressWarnings({ \"deprecation\", \"removal\" })")); + } + + @Test + @SuppressWarnings({ "deprecation", "removal" }) + void registerSeveralWarningsSuppressesThemOnType() { + this.codeWarnings.register("deprecation"); + this.codeWarnings.register("removal"); + compile(type -> { + this.codeWarnings.suppress(type); + type.addField(FieldSpec.builder(DeprecatedBean.class, "bean").build()); + type.addField(FieldSpec.builder(DeprecatedForRemovalBean.class, "another").build()); + }, compiled -> assertThat(compiled.getSourceFile()) + .contains("@SuppressWarnings({ \"deprecation\", \"removal\" })")); + } + + @Test + @SuppressWarnings("deprecation") + void detectDeprecationOnAnnotatedElementWithDeprecated() { + this.codeWarnings.detectDeprecation(DeprecatedBean.class); + assertThat(this.codeWarnings.getWarnings()).containsExactly("deprecation"); + } + + @Test + @SuppressWarnings("deprecation") + void detectDeprecationOnAnnotatedElementWhoseEnclosingElementIsDeprecated() { + this.codeWarnings.detectDeprecation(DeprecatedBean.Nested.class); + assertThat(this.codeWarnings.getWarnings()).containsExactly("deprecation"); + } + + @Test + @SuppressWarnings("removal") + void detectDeprecationOnAnnotatedElementWithDeprecatedForRemoval() { + this.codeWarnings.detectDeprecation(DeprecatedForRemovalBean.class); + assertThat(this.codeWarnings.getWarnings()).containsExactly("removal"); + } + + @Test + @SuppressWarnings("removal") + void detectDeprecationOnAnnotatedElementWhoseEnclosingElementIsDeprecatedForRemoval() { + this.codeWarnings.detectDeprecation(DeprecatedForRemovalBean.Nested.class); + assertThat(this.codeWarnings.getWarnings()).containsExactly("removal"); + } + + @ParameterizedTest + @MethodSource("resolvableTypesWithDeprecated") + void detectDeprecationOnResolvableTypeWithDeprecated(ResolvableType resolvableType) { + this.codeWarnings.detectDeprecation(resolvableType); + assertThat(this.codeWarnings.getWarnings()).containsExactly("deprecation"); + } + + @SuppressWarnings("deprecation") + static Stream resolvableTypesWithDeprecated() { + Class deprecatedBean = DeprecatedBean.class; + Class nested = DeprecatedBean.Nested.class; + return Stream.of( + Arguments.of(ResolvableType.forClass(deprecatedBean)), + Arguments.of(ResolvableType.forClass(nested)), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, deprecatedBean)), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, nested)), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, + ResolvableType.forClassWithGenerics(GenericBean.class, deprecatedBean))), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, + ResolvableType.forClassWithGenerics(GenericBean.class, nested))) + ); + } + + @ParameterizedTest + @MethodSource("resolvableTypesWithDeprecatedForRemoval") + void detectDeprecationOnResolvableTypeWithDeprecatedForRemoval(ResolvableType resolvableType) { + this.codeWarnings.detectDeprecation(resolvableType); + assertThat(this.codeWarnings.getWarnings()).containsExactly("removal"); + } + + @SuppressWarnings("removal") + static Stream resolvableTypesWithDeprecatedForRemoval() { + Class deprecatedBean = DeprecatedForRemovalBean.class; + Class nested = DeprecatedForRemovalBean.Nested.class; + return Stream.of( + Arguments.of(ResolvableType.forClass(deprecatedBean)), + Arguments.of(ResolvableType.forClass(nested)), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, deprecatedBean)), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, nested)), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, + ResolvableType.forClassWithGenerics(GenericBean.class, deprecatedBean))), + Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, + ResolvableType.forClassWithGenerics(GenericBean.class, nested))) + ); + } + + @Test + void toStringIncludeWarnings() { + this.codeWarnings.register("deprecation"); + this.codeWarnings.register("rawtypes"); + assertThat(this.codeWarnings).hasToString("CodeWarnings[deprecation, rawtypes]"); + } + + private void compileWithMethod(Consumer method, Consumer result) { + compile(type -> { + type.addModifiers(Modifier.PUBLIC); + Builder methodBuilder = MethodSpec.methodBuilder("apply") + .addModifiers(Modifier.PUBLIC); + method.accept(methodBuilder); + type.addMethod(methodBuilder.build()); + }, result); + } + + private void compile(Consumer type, Consumer result) { + DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + this.generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder); + typeBuilder.set(type); + this.generationContext.writeGeneratedContent(); + TEST_COMPILER.with(this.generationContext).compile(result); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java index 1ab505e3d35e..1333b2f1603a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,28 +16,42 @@ package org.springframework.beans.factory.aot; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.util.function.UnaryOperator; import org.junit.jupiter.api.Test; +import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.annotation.InjectAnnotationBeanPostProcessorTests.StringFactoryBean; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.beans.testfixture.beans.factory.StringFactoryBean; +import org.springframework.beans.testfixture.beans.factory.aot.DefaultSimpleBeanContract; import org.springframework.beans.testfixture.beans.factory.aot.GenericFactoryBean; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationCode; import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationsCode; import org.springframework.beans.testfixture.beans.factory.aot.NumberFactoryBean; import org.springframework.beans.testfixture.beans.factory.aot.SimpleBean; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanArrayFactoryBean; import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanConfiguration; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanContract; import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanFactoryBean; import org.springframework.core.ResolvableType; import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * Tests for {@link DefaultBeanRegistrationCodeFragments}. @@ -48,136 +62,249 @@ class DefaultBeanRegistrationCodeFragmentsTests { private final BeanRegistrationsCode beanRegistrationsCode = new MockBeanRegistrationsCode(new TestGenerationContext()); + private final GenerationContext generationContext = new TestGenerationContext(); + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + @Test + public void getTargetWithInstanceSupplier() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class); + beanDefinition.setInstanceSupplier(SimpleBean::new); + RegisteredBean registeredBean = registerTestBean(beanDefinition); + BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean); + assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean)) + .withMessageContaining("Error processing bean with name 'testBean': instance supplier is not supported"); + } + + @Test + public void getTargetWithInstanceSupplierAndResourceDescription() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class); + beanDefinition.setInstanceSupplier(SimpleBean::new); + beanDefinition.setResourceDescription("my test resource"); + RegisteredBean registeredBean = registerTestBean(beanDefinition); + BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean); + assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean)) + .withMessageContaining("Error processing bean with name 'testBean' defined in my test resource: " + + "instance supplier is not supported"); + } + @Test void getTargetOnConstructor() { - RegisteredBean registeredBean = registerTestBean(SimpleBean.class); - assertTarget(createInstance(registeredBean).getTarget(registeredBean, - SimpleBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + RegisteredBean registeredBean = registerTestBean(SimpleBean.class, + SimpleBean.class.getDeclaredConstructors()[0]); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), SimpleBean.class); } @Test void getTargetOnConstructorToPublicFactoryBean() { - RegisteredBean registeredBean = registerTestBean(SimpleBean.class); - assertTarget(createInstance(registeredBean).getTarget(registeredBean, - SimpleBeanFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + RegisteredBean registeredBean = registerTestBean(SimpleBean.class, + SimpleBeanFactoryBean.class.getDeclaredConstructors()[0]); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), SimpleBean.class); + } + + @Test + void getTargetOnConstructorToPublicFactoryBeanProducingArray() { + RegisteredBean registeredBean = registerTestBean(SimpleBean[].class, + SimpleBeanArrayFactoryBean.class.getDeclaredConstructors()[0]); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), SimpleBean.class); } @Test void getTargetOnConstructorToPublicGenericFactoryBeanExtractTargetFromFactoryBeanType() { - RegisteredBean registeredBean = registerTestBean(ResolvableType - .forClassWithGenerics(GenericFactoryBean.class, SimpleBean.class)); - assertTarget(createInstance(registeredBean).getTarget(registeredBean, - GenericFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + ResolvableType beanType = ResolvableType.forClassWithGenerics( + GenericFactoryBean.class, SimpleBean.class); + RegisteredBean registeredBean = registerTestBean(beanType, + GenericFactoryBean.class.getDeclaredConstructors()[0]); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), SimpleBean.class); } @Test void getTargetOnConstructorToPublicGenericFactoryBeanWithBoundExtractTargetFromFactoryBeanType() { - RegisteredBean registeredBean = registerTestBean(ResolvableType - .forClassWithGenerics(NumberFactoryBean.class, Integer.class)); - assertTarget(createInstance(registeredBean).getTarget(registeredBean, - NumberFactoryBean.class.getDeclaredConstructors()[0]), Integer.class); + ResolvableType beanType = ResolvableType.forClassWithGenerics( + NumberFactoryBean.class, Integer.class); + RegisteredBean registeredBean = registerTestBean(beanType, + NumberFactoryBean.class.getDeclaredConstructors()[0]); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), Integer.class); } @Test void getTargetOnConstructorToPublicGenericFactoryBeanUseBeanTypeAsFallback() { - RegisteredBean registeredBean = registerTestBean(SimpleBean.class); - assertTarget(createInstance(registeredBean).getTarget(registeredBean, - GenericFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + RegisteredBean registeredBean = registerTestBean(SimpleBean.class, + GenericFactoryBean.class.getDeclaredConstructors()[0]); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), SimpleBean.class); } @Test void getTargetOnConstructorToProtectedFactoryBean() { - RegisteredBean registeredBean = registerTestBean(SimpleBean.class); - assertTarget(createInstance(registeredBean).getTarget(registeredBean, - PrivilegedTestBeanFactoryBean.class.getDeclaredConstructors()[0]), + RegisteredBean registeredBean = registerTestBean(SimpleBean.class, + PrivilegedTestBeanFactoryBean.class.getDeclaredConstructors()[0]); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), PrivilegedTestBeanFactoryBean.class); } @Test void getTargetOnMethod() { - RegisteredBean registeredBean = registerTestBean(SimpleBean.class); Method method = ReflectionUtils.findMethod(SimpleBeanConfiguration.class, "simpleBean"); assertThat(method).isNotNull(); - assertTarget(createInstance(registeredBean).getTarget(registeredBean, method), + RegisteredBean registeredBean = registerTestBean(SimpleBean.class, method); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), SimpleBeanConfiguration.class); } + @Test // gh-32609 + void getTargetOnMethodFromInterface() { + this.beanFactory.registerBeanDefinition("configuration", + new RootBeanDefinition(DefaultSimpleBeanContract.class)); + Method method = ReflectionUtils.findMethod(SimpleBeanContract.class, "simpleBean"); + assertThat(method).isNotNull(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class); + applyConstructorOrFactoryMethod(beanDefinition, method); + beanDefinition.setFactoryBeanName("configuration"); + this.beanFactory.registerBeanDefinition("testBean", beanDefinition); + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "testBean"); + assertTarget(createInstance(registeredBean).getTarget(registeredBean), + DefaultSimpleBeanContract.class); + } + @Test void getTargetOnMethodWithInnerBeanInJavaPackage() { RegisteredBean registeredBean = registerTestBean(SimpleBean.class); - RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", - new RootBeanDefinition(String.class)); Method method = ReflectionUtils.findMethod(getClass(), "createString"); assertThat(method).isNotNull(); - assertTarget(createInstance(innerBean).getTarget(innerBean, method), getClass()); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + applyConstructorOrFactoryMethod(new RootBeanDefinition(String.class), method)); + assertTarget(createInstance(innerBean).getTarget(innerBean), getClass()); } @Test void getTargetOnConstructorWithInnerBeanInJavaPackage() { RegisteredBean registeredBean = registerTestBean(SimpleBean.class); - RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", new RootBeanDefinition(String.class)); - assertTarget(createInstance(innerBean).getTarget(innerBean, - String.class.getDeclaredConstructors()[0]), SimpleBean.class); + RootBeanDefinition innerBeanDefinition = applyConstructorOrFactoryMethod( + new RootBeanDefinition(String.class), String.class.getDeclaredConstructors()[0]); + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + innerBeanDefinition); + assertTarget(createInstance(innerBean).getTarget(innerBean), SimpleBean.class); } @Test void getTargetOnConstructorWithInnerBeanOnTypeInJavaPackage() { RegisteredBean registeredBean = registerTestBean(SimpleBean.class); + RootBeanDefinition innerBeanDefinition = applyConstructorOrFactoryMethod( + new RootBeanDefinition(StringFactoryBean.class), + StringFactoryBean.class.getDeclaredConstructors()[0]); RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", - new RootBeanDefinition(StringFactoryBean.class)); - assertTarget(createInstance(innerBean).getTarget(innerBean, - StringFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + innerBeanDefinition); + assertTarget(createInstance(innerBean).getTarget(innerBean), SimpleBean.class); } @Test void getTargetOnMethodWithInnerBeanInRegularPackage() { RegisteredBean registeredBean = registerTestBean(DummyFactory.class); - RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", - new RootBeanDefinition(SimpleBean.class)); Method method = ReflectionUtils.findMethod(SimpleBeanConfiguration.class, "simpleBean"); assertThat(method).isNotNull(); - assertTarget(createInstance(innerBean).getTarget(innerBean, method), + RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", + applyConstructorOrFactoryMethod(new RootBeanDefinition(SimpleBean.class), method)); + assertTarget(createInstance(innerBean).getTarget(innerBean), SimpleBeanConfiguration.class); } @Test void getTargetOnConstructorWithInnerBeanInRegularPackage() { RegisteredBean registeredBean = registerTestBean(DummyFactory.class); + RootBeanDefinition innerBeanDefinition = applyConstructorOrFactoryMethod( + new RootBeanDefinition(SimpleBean.class), SimpleBean.class.getDeclaredConstructors()[0]); RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", - new RootBeanDefinition(SimpleBean.class)); - assertTarget(createInstance(innerBean).getTarget(innerBean, - SimpleBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + innerBeanDefinition); + assertTarget(createInstance(innerBean).getTarget(innerBean), SimpleBean.class); } @Test void getTargetOnConstructorWithInnerBeanOnFactoryBeanOnTypeInRegularPackage() { RegisteredBean registeredBean = registerTestBean(DummyFactory.class); + RootBeanDefinition innerBeanDefinition = applyConstructorOrFactoryMethod( + new RootBeanDefinition(SimpleBean.class), + SimpleBeanFactoryBean.class.getDeclaredConstructors()[0]); RegisteredBean innerBean = RegisteredBean.ofInnerBean(registeredBean, "innerTestBean", - new RootBeanDefinition(SimpleBean.class)); - assertTarget(createInstance(innerBean).getTarget(innerBean, - SimpleBeanFactoryBean.class.getDeclaredConstructors()[0]), SimpleBean.class); + innerBeanDefinition); + assertTarget(createInstance(innerBean).getTarget(innerBean), SimpleBean.class); + } + + @Test + void customizedGetTargetDoesNotResolveInstantiationDescriptor() { + RegisteredBean registeredBean = spy(registerTestBean(SimpleBean.class)); + BeanRegistrationCodeFragments customCodeFragments = createCustomCodeFragments(registeredBean, codeFragments -> new BeanRegistrationCodeFragmentsDecorator(codeFragments) { + @Override + public ClassName getTarget(RegisteredBean registeredBean) { + return ClassName.get(String.class); + } + }); + assertTarget(customCodeFragments.getTarget(registeredBean), String.class); + verify(registeredBean, never()).resolveInstantiationDescriptor(); + } + + @Test + void customizedGenerateInstanceSupplierCodeDoesNotResolveInstantiationDescriptor() { + RegisteredBean registeredBean = spy(registerTestBean(SimpleBean.class)); + BeanRegistrationCodeFragments customCodeFragments = createCustomCodeFragments(registeredBean, codeFragments -> new BeanRegistrationCodeFragmentsDecorator(codeFragments) { + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + return CodeBlock.of("// Hello"); + } + }); + assertThat(customCodeFragments.generateInstanceSupplierCode(this.generationContext, + new MockBeanRegistrationCode(this.generationContext), false)).hasToString("// Hello"); + verify(registeredBean, never()).resolveInstantiationDescriptor(); + } + + private BeanRegistrationCodeFragments createCustomCodeFragments(RegisteredBean registeredBean, UnaryOperator customFragments) { + BeanRegistrationAotContribution aotContribution = BeanRegistrationAotContribution. + withCustomCodeFragments(customFragments); + BeanRegistrationCodeFragments defaultCodeFragments = createInstance(registeredBean); + return aotContribution.customizeBeanRegistrationCodeFragments( + this.generationContext, defaultCodeFragments); } private void assertTarget(ClassName target, Class expected) { assertThat(target).isEqualTo(ClassName.get(expected)); } - private RegisteredBean registerTestBean(Class beanType) { - this.beanFactory.registerBeanDefinition("testBean", - new RootBeanDefinition(beanType)); + return registerTestBean(beanType, null); + } + + private RegisteredBean registerTestBean(Class beanType, + @Nullable Executable constructorOrFactoryMethod) { + this.beanFactory.registerBeanDefinition("testBean", applyConstructorOrFactoryMethod( + new RootBeanDefinition(beanType), constructorOrFactoryMethod)); return RegisteredBean.of(this.beanFactory, "testBean"); } - private RegisteredBean registerTestBean(ResolvableType beanType) { + private RegisteredBean registerTestBean(ResolvableType beanType, + @Nullable Executable constructorOrFactoryMethod) { RootBeanDefinition beanDefinition = new RootBeanDefinition(); beanDefinition.setTargetType(beanType); + return registerTestBean(applyConstructorOrFactoryMethod( + beanDefinition, constructorOrFactoryMethod)); + } + + private RegisteredBean registerTestBean(RootBeanDefinition beanDefinition) { this.beanFactory.registerBeanDefinition("testBean", beanDefinition); return RegisteredBean.of(this.beanFactory, "testBean"); } + private RootBeanDefinition applyConstructorOrFactoryMethod(RootBeanDefinition beanDefinition, + @Nullable Executable constructorOrFactoryMethod) { + + if (constructorOrFactoryMethod instanceof Method method) { + beanDefinition.setResolvedFactoryMethod(method); + } + else if (constructorOrFactoryMethod instanceof Constructor constructor) { + beanDefinition.setAttribute(RootBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, constructor); + } + return beanDefinition; + } + private BeanRegistrationCodeFragments createInstance(RegisteredBean registeredBean) { return new DefaultBeanRegistrationCodeFragments(this.beanRegistrationsCode, registeredBean, new BeanDefinitionMethodGeneratorFactory(this.beanFactory)); @@ -191,7 +318,7 @@ static String createString() { static class PrivilegedTestBeanFactoryBean implements FactoryBean { @Override - public SimpleBean getObject() throws Exception { + public SimpleBean getObject() { return new SimpleBean(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java index b849b3702209..84e5480494ee 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ package org.springframework.beans.factory.aot; -import java.lang.reflect.Executable; import java.util.function.BiConsumer; import java.util.function.Supplier; import javax.lang.model.element.Modifier; import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GeneratedClass; @@ -36,14 +37,24 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.InstanceSupplier; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.TestBeanWithPrivateConstructor; +import org.springframework.beans.testfixture.beans.factory.aot.DefaultSimpleBeanContract; import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBean; +import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanContract; import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration; import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.EnvironmentAwareComponent; import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.NoDependencyComponent; import org.springframework.beans.testfixture.beans.factory.generator.SimpleConfiguration; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedConstructor; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalBean; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalConstructor; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalMemberConfiguration; +import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedMemberConfiguration; import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolder; import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolderFactoryBean; import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory; @@ -57,6 +68,7 @@ import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link InstanceSupplierCodeGenerator}. @@ -68,18 +80,20 @@ class InstanceSupplierCodeGeneratorTests { private final TestGenerationContext generationContext; + private final DefaultListableBeanFactory beanFactory; + InstanceSupplierCodeGeneratorTests() { this.generationContext = new TestGenerationContext(); + this.beanFactory = new DefaultListableBeanFactory(); } @Test void generateWhenHasDefaultConstructor() { BeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - TestBean bean = getBean(beanFactory, beanDefinition, instanceSupplier); + compile(beanDefinition, (instanceSupplier, compiled) -> { + TestBean bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(TestBean.class); assertThat(compiled.getSourceFile()) .contains("InstanceSupplier.using(TestBean::new)"); @@ -91,13 +105,10 @@ void generateWhenHasDefaultConstructor() { @Test void generateWhenHasConstructorWithParameter() { BeanDefinition beanDefinition = new RootBeanDefinition(InjectionComponent.class); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerSingleton("injected", "injected"); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - InjectionComponent bean = getBean(beanFactory, beanDefinition, - instanceSupplier); - assertThat(bean).isInstanceOf(InjectionComponent.class).extracting("bean") - .isEqualTo("injected"); + this.beanFactory.registerSingleton("injected", "injected"); + compile(beanDefinition, (instanceSupplier, compiled) -> { + InjectionComponent bean = getBean(beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(InjectionComponent.class).extracting("bean").isEqualTo("injected"); }); assertThat(getReflectionHints().getTypeHint(InjectionComponent.class)) .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); @@ -105,13 +116,10 @@ void generateWhenHasConstructorWithParameter() { @Test void generateWhenHasConstructorWithInnerClassAndDefaultConstructor() { - RootBeanDefinition beanDefinition = new RootBeanDefinition( - NoDependencyComponent.class); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - NoDependencyComponent bean = getBean(beanFactory, beanDefinition, - instanceSupplier); + RootBeanDefinition beanDefinition = new RootBeanDefinition(NoDependencyComponent.class); + this.beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); + compile(beanDefinition, (instanceSupplier, compiled) -> { + NoDependencyComponent bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(NoDependencyComponent.class); assertThat(compiled.getSourceFile()).contains( "getBeanFactory().getBean(InnerComponentConfiguration.class).new NoDependencyComponent()"); @@ -122,14 +130,11 @@ void generateWhenHasConstructorWithInnerClassAndDefaultConstructor() { @Test void generateWhenHasConstructorWithInnerClassAndParameter() { - BeanDefinition beanDefinition = new RootBeanDefinition( - EnvironmentAwareComponent.class); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); - beanFactory.registerSingleton("environment", new StandardEnvironment()); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - EnvironmentAwareComponent bean = getBean(beanFactory, beanDefinition, - instanceSupplier); + BeanDefinition beanDefinition = new RootBeanDefinition(EnvironmentAwareComponent.class); + this.beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); + this.beanFactory.registerSingleton("environment", new StandardEnvironment()); + compile(beanDefinition, (instanceSupplier, compiled) -> { + EnvironmentAwareComponent bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(EnvironmentAwareComponent.class); assertThat(compiled.getSourceFile()).contains( "getBeanFactory().getBean(InnerComponentConfiguration.class).new EnvironmentAwareComponent("); @@ -140,12 +145,10 @@ void generateWhenHasConstructorWithInnerClassAndParameter() { @Test void generateWhenHasConstructorWithGeneric() { - BeanDefinition beanDefinition = new RootBeanDefinition( - NumberHolderFactoryBean.class); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerSingleton("number", 123); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - NumberHolder bean = getBean(beanFactory, beanDefinition, instanceSupplier); + BeanDefinition beanDefinition = new RootBeanDefinition(NumberHolderFactoryBean.class); + this.beanFactory.registerSingleton("number", 123); + compile(beanDefinition, (instanceSupplier, compiled) -> { + NumberHolder bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(NumberHolder.class); assertThat(bean).extracting("number").isNull(); // No property actually set assertThat(compiled.getSourceFile()).contains("NumberHolderFactoryBean::new"); @@ -156,12 +159,9 @@ void generateWhenHasConstructorWithGeneric() { @Test void generateWhenHasPrivateConstructor() { - BeanDefinition beanDefinition = new RootBeanDefinition( - TestBeanWithPrivateConstructor.class); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - TestBeanWithPrivateConstructor bean = getBean(beanFactory, beanDefinition, - instanceSupplier); + BeanDefinition beanDefinition = new RootBeanDefinition(TestBeanWithPrivateConstructor.class); + compile(beanDefinition, (instanceSupplier, compiled) -> { + TestBeanWithPrivateConstructor bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(TestBeanWithPrivateConstructor.class); assertThat(compiled.getSourceFile()) .contains("return BeanInstanceSupplier.forConstructor();"); @@ -175,11 +175,10 @@ void generateWhenHasFactoryMethodWithNoArg() { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(String.class) .setFactoryMethodOnBean("stringBean", "config").getBeanDefinition(); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - String bean = getBean(beanFactory, beanDefinition, instanceSupplier); + compile(beanDefinition, (instanceSupplier, compiled) -> { + String bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(String.class); assertThat(bean).isEqualTo("Hello"); assertThat(compiled.getSourceFile()).contains( @@ -189,17 +188,33 @@ void generateWhenHasFactoryMethodWithNoArg() { .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); } + @Test + void generateWhenHasFactoryMethodOnInterface() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SimpleBean.class) + .setFactoryMethodOnBean("simpleBean", "config").getBeanDefinition(); + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .rootBeanDefinition(DefaultSimpleBeanContract.class).getBeanDefinition()); + compile(beanDefinition, (instanceSupplier, compiled) -> { + Object bean = getBean(beanDefinition, instanceSupplier); + assertThat(bean).isInstanceOf(SimpleBean.class); + assertThat(compiled.getSourceFile()).contains( + "getBeanFactory().getBean(DefaultSimpleBeanContract.class).simpleBean()"); + }); + assertThat(getReflectionHints().getTypeHint(SimpleBeanContract.class)) + .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + } + @Test void generateWhenHasPrivateStaticFactoryMethodWithNoArg() { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(String.class) .setFactoryMethodOnBean("privateStaticStringBean", "config") .getBeanDefinition(); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - String bean = getBean(beanFactory, beanDefinition, instanceSupplier); + compile(beanDefinition, (instanceSupplier, compiled) -> { + String bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(String.class); assertThat(bean).isEqualTo("Hello"); assertThat(compiled.getSourceFile()) @@ -215,11 +230,10 @@ void generateWhenHasStaticFactoryMethodWithNoArg() { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(Integer.class) .setFactoryMethodOnBean("integerBean", "config").getBeanDefinition(); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - Integer bean = getBean(beanFactory, beanDefinition, instanceSupplier); + compile(beanDefinition, (instanceSupplier, compiled) -> { + Integer bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(Integer.class); assertThat(bean).isEqualTo(42); assertThat(compiled.getSourceFile()) @@ -236,13 +250,12 @@ void generateWhenHasStaticFactoryMethodWithArg() { .setFactoryMethodOnBean("create", "config").getBeanDefinition(); beanDefinition.setResolvedFactoryMethod(ReflectionUtils .findMethod(SampleFactory.class, "create", Number.class, String.class)); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder .genericBeanDefinition(SampleFactory.class).getBeanDefinition()); - beanFactory.registerSingleton("number", 42); - beanFactory.registerSingleton("string", "test"); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - String bean = getBean(beanFactory, beanDefinition, instanceSupplier); + this.beanFactory.registerSingleton("number", 42); + this.beanFactory.registerSingleton("string", "test"); + compile(beanDefinition, (instanceSupplier, compiled) -> { + String bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(String.class); assertThat(bean).isEqualTo("42test"); assertThat(compiled.getSourceFile()).contains("SampleFactory.create("); @@ -257,11 +270,10 @@ void generateWhenHasStaticFactoryMethodCheckedException() { .rootBeanDefinition(Integer.class) .setFactoryMethodOnBean("throwingIntegerBean", "config") .getBeanDefinition(); - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder .genericBeanDefinition(SimpleConfiguration.class).getBeanDefinition()); - compile(beanFactory, beanDefinition, (instanceSupplier, compiled) -> { - Integer bean = getBean(beanFactory, beanDefinition, instanceSupplier); + compile(beanDefinition, (instanceSupplier, compiled) -> { + Integer bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(Integer.class); assertThat(bean).isEqualTo(42); assertThat(compiled.getSourceFile()).doesNotContain(") throws Exception {"); @@ -270,6 +282,108 @@ void generateWhenHasStaticFactoryMethodCheckedException() { .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); } + @Nested + @SuppressWarnings("deprecation") + class DeprecationTests { + + private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem() + .withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror"); + + @Test + @Disabled("Need to move to a separate method so that the warning can be suppressed") + void generateWhenTargetClassIsDeprecated() { + compileAndCheckWarnings(new RootBeanDefinition(DeprecatedBean.class)); + } + + @Test + void generateWhenTargetConstructorIsDeprecated() { + compileAndCheckWarnings(new RootBeanDefinition(DeprecatedConstructor.class)); + } + + @Test + void generateWhenTargetFactoryMethodIsDeprecated() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setFactoryMethodOnBean("deprecatedString", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition()); + compileAndCheckWarnings(beanDefinition); + } + + @Test + void generateWhenTargetFactoryMethodParameterIsDeprecated() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setFactoryMethodOnBean("deprecatedParameter", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition()); + beanFactory.registerBeanDefinition("parameter", new RootBeanDefinition(DeprecatedBean.class)); + compileAndCheckWarnings(beanDefinition); + } + + @Test + void generateWhenTargetFactoryMethodReturnTypeIsDeprecated() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(DeprecatedBean.class) + .setFactoryMethodOnBean("deprecatedReturnType", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition()); + compileAndCheckWarnings(beanDefinition); + } + + private void compileAndCheckWarnings(BeanDefinition beanDefinition) { + assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition, + ((instanceSupplier, compiled) -> {}))); + } + + } + + @Nested + @SuppressWarnings("removal") + class DeprecationForRemovalTests { + + private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem() + .withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror"); + + @Test + @Disabled("Need to move to a separate method so that the warning can be suppressed") + void generateWhenTargetClassIsDeprecatedForRemoval() { + compileAndCheckWarnings(new RootBeanDefinition(DeprecatedForRemovalBean.class)); + } + + @Test + void generateWhenTargetConstructorIsDeprecatedForRemoval() { + compileAndCheckWarnings(new RootBeanDefinition(DeprecatedForRemovalConstructor.class)); + } + + @Test + void generateWhenTargetFactoryMethodIsDeprecatedForRemoval() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setFactoryMethodOnBean("deprecatedString", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition()); + compileAndCheckWarnings(beanDefinition); + } + + @Test + void generateWhenTargetFactoryMethodParameterIsDeprecatedForRemoval() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String.class) + .setFactoryMethodOnBean("deprecatedParameter", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition()); + beanFactory.registerBeanDefinition("parameter", new RootBeanDefinition(DeprecatedForRemovalBean.class)); + compileAndCheckWarnings(beanDefinition); + } + + private void compileAndCheckWarnings(BeanDefinition beanDefinition) { + assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition, + ((instanceSupplier, compiled) -> {}))); + } + + } + private ReflectionHints getReflectionHints() { return this.generationContext.getRuntimeHints().reflection(); } @@ -287,17 +401,20 @@ private ThrowingConsumer hasMode(ExecutableMode mode) { } @SuppressWarnings("unchecked") - private T getBean(DefaultListableBeanFactory beanFactory, - BeanDefinition beanDefinition, InstanceSupplier instanceSupplier) { + private T getBean(BeanDefinition beanDefinition, InstanceSupplier instanceSupplier) { ((RootBeanDefinition) beanDefinition).setInstanceSupplier(instanceSupplier); - beanFactory.registerBeanDefinition("testBean", beanDefinition); - return (T) beanFactory.getBean("testBean"); + this.beanFactory.registerBeanDefinition("testBean", beanDefinition); + return (T) this.beanFactory.getBean("testBean"); + } + + private void compile(BeanDefinition beanDefinition, BiConsumer, Compiled> result) { + compile(TestCompiler.forSystem(), beanDefinition, result); } - private void compile(DefaultListableBeanFactory beanFactory, BeanDefinition beanDefinition, + private void compile(TestCompiler testCompiler, BeanDefinition beanDefinition, BiConsumer, Compiled> result) { - DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(beanFactory); + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(this.beanFactory); freshBeanFactory.registerBeanDefinition("testBean", beanDefinition); RegisteredBean registeredBean = RegisteredBean.of(freshBeanFactory, "testBean"); DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); @@ -305,9 +422,9 @@ private void compile(DefaultListableBeanFactory beanFactory, BeanDefinition bean InstanceSupplierCodeGenerator generator = new InstanceSupplierCodeGenerator( this.generationContext, generateClass.getName(), generateClass.getMethods(), false); - Executable constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod(); - assertThat(constructorOrFactoryMethod).isNotNull(); - CodeBlock generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod); + InstantiationDescriptor instantiationDescriptor = registeredBean.resolveInstantiationDescriptor(); + assertThat(instantiationDescriptor).isNotNull(); + CodeBlock generatedCode = generator.generateCode(registeredBean, instantiationDescriptor); typeBuilder.set(type -> { type.addModifiers(Modifier.PUBLIC); type.addSuperinterface(ParameterizedTypeName.get(Supplier.class, InstanceSupplier.class)); @@ -317,8 +434,8 @@ private void compile(DefaultListableBeanFactory beanFactory, BeanDefinition bean .addStatement("return $L", generatedCode).build()); }); this.generationContext.writeGeneratedContent(); - TestCompiler.forSystem().with(this.generationContext).compile(compiled -> - result.accept((InstanceSupplier) compiled.getInstance(Supplier.class).get(), compiled)); + testCompiler.with(this.generationContext).compile(compiled -> result.accept( + (InstanceSupplier) compiled.getInstance(Supplier.class).get(), compiled)); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java index 48a87a9a7739..0ec429a10c8c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,10 +41,10 @@ * @author Chris Beams * @since 31.07.2004 */ -public class CustomEditorConfigurerTests { +class CustomEditorConfigurerTests { @Test - public void testCustomEditorConfigurerWithPropertyEditorRegistrar() throws ParseException { + void testCustomEditorConfigurerWithPropertyEditorRegistrar() throws ParseException { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); CustomEditorConfigurer cec = new CustomEditorConfigurer(); final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMAN); @@ -70,7 +70,7 @@ public void testCustomEditorConfigurerWithPropertyEditorRegistrar() throws Parse } @Test - public void testCustomEditorConfigurerWithEditorAsClass() throws ParseException { + void testCustomEditorConfigurerWithEditorAsClass() throws ParseException { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); CustomEditorConfigurer cec = new CustomEditorConfigurer(); Map, Class> editors = new HashMap<>(); @@ -90,7 +90,7 @@ public void testCustomEditorConfigurerWithEditorAsClass() throws ParseException } @Test - public void testCustomEditorConfigurerWithRequiredTypeArray() throws ParseException { + void testCustomEditorConfigurerWithRequiredTypeArray() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); CustomEditorConfigurer cec = new CustomEditorConfigurer(); Map, Class> editors = new HashMap<>(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomScopeConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomScopeConfigurerTests.java index 1eb97f98e580..a3938dbacb4e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomScopeConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomScopeConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,13 +29,13 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for {@link CustomScopeConfigurer}. + * Tests for {@link CustomScopeConfigurer}. * * @author Rick Evans * @author Juergen Hoeller * @author Chris Beams */ -public class CustomScopeConfigurerTests { +class CustomScopeConfigurerTests { private static final String FOO_SCOPE = "fooScope"; @@ -43,13 +43,13 @@ public class CustomScopeConfigurerTests { @Test - public void testWithNoScopes() { + void testWithNoScopes() { CustomScopeConfigurer figurer = new CustomScopeConfigurer(); figurer.postProcessBeanFactory(factory); } @Test - public void testSunnyDayWithBonaFideScopeInstance() { + void testSunnyDayWithBonaFideScopeInstance() { Scope scope = mock(); factory.registerScope(FOO_SCOPE, scope); Map scopes = new HashMap<>(); @@ -60,27 +60,27 @@ public void testSunnyDayWithBonaFideScopeInstance() { } @Test - public void testSunnyDayWithBonaFideScopeClass() { + void testSunnyDayWithBonaFideScopeClass() { Map scopes = new HashMap<>(); scopes.put(FOO_SCOPE, NoOpScope.class); CustomScopeConfigurer figurer = new CustomScopeConfigurer(); figurer.setScopes(scopes); figurer.postProcessBeanFactory(factory); - assertThat(factory.getRegisteredScope(FOO_SCOPE) instanceof NoOpScope).isTrue(); + assertThat(factory.getRegisteredScope(FOO_SCOPE)).isInstanceOf(NoOpScope.class); } @Test - public void testSunnyDayWithBonaFideScopeClassName() { + void testSunnyDayWithBonaFideScopeClassName() { Map scopes = new HashMap<>(); scopes.put(FOO_SCOPE, NoOpScope.class.getName()); CustomScopeConfigurer figurer = new CustomScopeConfigurer(); figurer.setScopes(scopes); figurer.postProcessBeanFactory(factory); - assertThat(factory.getRegisteredScope(FOO_SCOPE) instanceof NoOpScope).isTrue(); + assertThat(factory.getRegisteredScope(FOO_SCOPE)).isInstanceOf(NoOpScope.class); } @Test - public void testWhereScopeMapHasNullScopeValueInEntrySet() { + void testWhereScopeMapHasNullScopeValueInEntrySet() { Map scopes = new HashMap<>(); scopes.put(FOO_SCOPE, null); CustomScopeConfigurer figurer = new CustomScopeConfigurer(); @@ -90,7 +90,7 @@ public void testWhereScopeMapHasNullScopeValueInEntrySet() { } @Test - public void testWhereScopeMapHasNonScopeInstanceInEntrySet() { + void testWhereScopeMapHasNonScopeInstanceInEntrySet() { Map scopes = new HashMap<>(); scopes.put(FOO_SCOPE, this); // <-- not a valid value... CustomScopeConfigurer figurer = new CustomScopeConfigurer(); @@ -101,7 +101,7 @@ public void testWhereScopeMapHasNonScopeInstanceInEntrySet() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Test - public void testWhereScopeMapHasNonStringTypedScopeNameInKeySet() { + void testWhereScopeMapHasNonStringTypedScopeNameInKeySet() { Map scopes = new HashMap(); scopes.put(this, new NoOpScope()); // <-- not a valid value (the key)... CustomScopeConfigurer figurer = new CustomScopeConfigurer(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/DeprecatedBeanWarnerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/DeprecatedBeanWarnerTests.java index 2be93f0d400e..d9a6023e3c7b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/DeprecatedBeanWarnerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/DeprecatedBeanWarnerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ /** * @author Arjen Poutsma */ -public class DeprecatedBeanWarnerTests { +class DeprecatedBeanWarnerTests { private String beanName; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests.java index b697500fb343..9ed527c73e2b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,16 @@ import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; /** - * Unit tests for {@link FieldRetrievingFactoryBean}. + * Tests for {@link FieldRetrievingFactoryBean}. * * @author Juergen Hoeller * @author Chris Beams * @since 31.07.2004 */ -public class FieldRetrievingFactoryBeanTests { +class FieldRetrievingFactoryBeanTests { @Test - public void testStaticField() throws Exception { + void testStaticField() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setStaticField("java.sql.Connection.TRANSACTION_SERIALIZABLE"); fr.afterPropertiesSet(); @@ -45,7 +45,7 @@ public void testStaticField() throws Exception { } @Test - public void testStaticFieldWithWhitespace() throws Exception { + void testStaticFieldWithWhitespace() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setStaticField(" java.sql.Connection.TRANSACTION_SERIALIZABLE "); fr.afterPropertiesSet(); @@ -53,7 +53,7 @@ public void testStaticFieldWithWhitespace() throws Exception { } @Test - public void testStaticFieldViaClassAndFieldName() throws Exception { + void testStaticFieldViaClassAndFieldName() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setTargetClass(Connection.class); fr.setTargetField("TRANSACTION_SERIALIZABLE"); @@ -62,7 +62,7 @@ public void testStaticFieldViaClassAndFieldName() throws Exception { } @Test - public void testNonStaticField() throws Exception { + void testNonStaticField() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); PublicFieldHolder target = new PublicFieldHolder(); fr.setTargetObject(target); @@ -72,7 +72,7 @@ public void testNonStaticField() throws Exception { } @Test - public void testNothingButBeanName() throws Exception { + void testNothingButBeanName() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setBeanName("java.sql.Connection.TRANSACTION_SERIALIZABLE"); fr.afterPropertiesSet(); @@ -80,7 +80,7 @@ public void testNothingButBeanName() throws Exception { } @Test - public void testJustTargetField() throws Exception { + void testJustTargetField() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setTargetField("TRANSACTION_SERIALIZABLE"); try { @@ -91,7 +91,7 @@ public void testJustTargetField() throws Exception { } @Test - public void testJustTargetClass() throws Exception { + void testJustTargetClass() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setTargetClass(Connection.class); try { @@ -102,7 +102,7 @@ public void testJustTargetClass() throws Exception { } @Test - public void testJustTargetObject() throws Exception { + void testJustTargetObject() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setTargetObject(new PublicFieldHolder()); try { @@ -113,7 +113,7 @@ public void testJustTargetObject() throws Exception { } @Test - public void testWithConstantOnClassWithPackageLevelVisibility() throws Exception { + void testWithConstantOnClassWithPackageLevelVisibility() throws Exception { FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); fr.setBeanName("org.springframework.beans.testfixture.beans.PackageLevelVisibleBean.CONSTANT"); fr.afterPropertiesSet(); @@ -121,7 +121,7 @@ public void testWithConstantOnClassWithPackageLevelVisibility() throws Exception } @Test - public void testBeanNameSyntaxWithBeanFactory() throws Exception { + void testBeanNameSyntaxWithBeanFactory() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(FieldRetrievingFactoryBeanTests.class, "context.xml")); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java index ff1af3ed421d..0be2f8cbe240 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,17 +31,17 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link MethodInvokingFactoryBean} and {@link MethodInvokingBean}. + * Tests for {@link MethodInvokingFactoryBean} and {@link MethodInvokingBean}. * * @author Colin Sampaleanu * @author Juergen Hoeller * @author Chris Beams * @since 21.11.2003 */ -public class MethodInvokingFactoryBeanTests { +class MethodInvokingFactoryBeanTests { @Test - public void testParameterValidation() throws Exception { + void testParameterValidation() throws Exception { // assert that only static OR non-static are set, but not both or none MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); @@ -91,7 +91,7 @@ public void testParameterValidation() throws Exception { } @Test - public void testGetObjectType() throws Exception { + void testGetObjectType() throws Exception { TestClass1 tc1 = new TestClass1(); MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); mcfb = new MethodInvokingFactoryBean(); @@ -127,7 +127,7 @@ public void testGetObjectType() throws Exception { } @Test - public void testGetObject() throws Exception { + void testGetObject() throws Exception { // singleton, non-static TestClass1 tc1 = new TestClass1(); MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); @@ -190,7 +190,7 @@ public void testGetObject() throws Exception { } @Test - public void testArgumentConversion() throws Exception { + void testArgumentConversion() throws Exception { MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); mcfb.setTargetClass(TestClass1.class); mcfb.setTargetMethod("supertypes"); @@ -224,7 +224,7 @@ public void testArgumentConversion() throws Exception { } @Test - public void testInvokeWithNullArgument() throws Exception { + void testInvokeWithNullArgument() throws Exception { MethodInvoker methodInvoker = new MethodInvoker(); methodInvoker.setTargetClass(TestClass1.class); methodInvoker.setTargetMethod("nullArgument"); @@ -234,7 +234,7 @@ public void testInvokeWithNullArgument() throws Exception { } @Test - public void testInvokeWithIntArgument() throws Exception { + void testInvokeWithIntArgument() throws Exception { ArgumentConvertingMethodInvoker methodInvoker = new ArgumentConvertingMethodInvoker(); methodInvoker.setTargetClass(TestClass1.class); methodInvoker.setTargetMethod("intArgument"); @@ -251,7 +251,7 @@ public void testInvokeWithIntArgument() throws Exception { } @Test - public void testInvokeWithIntArguments() throws Exception { + void testInvokeWithIntArguments() throws Exception { MethodInvokingBean methodInvoker = new MethodInvokingBean(); methodInvoker.setTargetClass(TestClass1.class); methodInvoker.setTargetMethod("intArguments"); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java index 7088a51b1a14..1dc9482f1cb6 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,13 +41,13 @@ * @author Rick Evans * @author Chris Beams */ -public class ObjectFactoryCreatingFactoryBeanTests { +class ObjectFactoryCreatingFactoryBeanTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setup() { + void setup() { this.beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( qualifiedResource(ObjectFactoryCreatingFactoryBeanTests.class, "context.xml")); @@ -55,13 +55,13 @@ public void setup() { } @AfterEach - public void close() { + void close() { this.beanFactory.setSerializationId(null); } @Test - public void testFactoryOperation() { + void testFactoryOperation() { FactoryTestBean testBean = beanFactory.getBean("factoryTestBean", FactoryTestBean.class); ObjectFactory objectFactory = testBean.getObjectFactory(); @@ -71,7 +71,7 @@ public void testFactoryOperation() { } @Test - public void testFactorySerialization() throws Exception { + void testFactorySerialization() throws Exception { FactoryTestBean testBean = beanFactory.getBean("factoryTestBean", FactoryTestBean.class); ObjectFactory objectFactory = testBean.getObjectFactory(); @@ -83,7 +83,7 @@ public void testFactorySerialization() throws Exception { } @Test - public void testProviderOperation() { + void testProviderOperation() { ProviderTestBean testBean = beanFactory.getBean("providerTestBean", ProviderTestBean.class); Provider provider = testBean.getProvider(); @@ -93,7 +93,7 @@ public void testProviderOperation() { } @Test - public void testProviderSerialization() throws Exception { + void testProviderSerialization() throws Exception { ProviderTestBean testBean = beanFactory.getBean("providerTestBean", ProviderTestBean.class); Provider provider = testBean.getProvider(); @@ -105,7 +105,7 @@ public void testProviderSerialization() throws Exception { } @Test - public void testDoesNotComplainWhenTargetBeanNameRefersToSingleton() throws Exception { + void testDoesNotComplainWhenTargetBeanNameRefersToSingleton() throws Exception { final String targetBeanName = "singleton"; final String expectedSingleton = "Alicia Keys"; @@ -122,14 +122,14 @@ public void testDoesNotComplainWhenTargetBeanNameRefersToSingleton() throws Exce } @Test - public void testWhenTargetBeanNameIsNull() throws Exception { + void testWhenTargetBeanNameIsNull() { assertThatIllegalArgumentException().as( "'targetBeanName' property not set").isThrownBy( new ObjectFactoryCreatingFactoryBean()::afterPropertiesSet); } @Test - public void testWhenTargetBeanNameIsEmptyString() throws Exception { + void testWhenTargetBeanNameIsEmptyString() { ObjectFactoryCreatingFactoryBean factory = new ObjectFactoryCreatingFactoryBean(); factory.setTargetBeanName(""); assertThatIllegalArgumentException().as( @@ -138,7 +138,7 @@ public void testWhenTargetBeanNameIsEmptyString() throws Exception { } @Test - public void testWhenTargetBeanNameIsWhitespacedString() throws Exception { + void testWhenTargetBeanNameIsWhitespacedString() { ObjectFactoryCreatingFactoryBean factory = new ObjectFactoryCreatingFactoryBean(); factory.setTargetBeanName(" \t"); assertThatIllegalArgumentException().as( @@ -147,7 +147,7 @@ public void testWhenTargetBeanNameIsWhitespacedString() throws Exception { } @Test - public void testEnsureOFBFBReportsThatItActuallyCreatesObjectFactoryInstances() { + void testEnsureOFBFBReportsThatItActuallyCreatesObjectFactoryInstances() { assertThat(new ObjectFactoryCreatingFactoryBean().getObjectType()).as("Must be reporting that it creates ObjectFactory instances (as per class contract).").isEqualTo(ObjectFactory.class); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertiesFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertiesFactoryBeanTests.java index a04c2d768da6..5f9d90f022d3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertiesFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertiesFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,20 +26,20 @@ import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; /** - * Unit tests for {@link PropertiesFactoryBean}. + * Tests for {@link PropertiesFactoryBean}. * * @author Juergen Hoeller * @author Chris Beams * @since 01.11.2003 */ -public class PropertiesFactoryBeanTests { +class PropertiesFactoryBeanTests { private static final Class CLASS = PropertiesFactoryBeanTests.class; private static final Resource TEST_PROPS = qualifiedResource(CLASS, "test.properties"); private static final Resource TEST_PROPS_XML = qualifiedResource(CLASS, "test.properties.xml"); @Test - public void testWithPropertiesFile() throws Exception { + void testWithPropertiesFile() throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); pfb.setLocation(TEST_PROPS); pfb.afterPropertiesSet(); @@ -48,7 +48,7 @@ public void testWithPropertiesFile() throws Exception { } @Test - public void testWithPropertiesXmlFile() throws Exception { + void testWithPropertiesXmlFile() throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); pfb.setLocation(TEST_PROPS_XML); pfb.afterPropertiesSet(); @@ -57,7 +57,7 @@ public void testWithPropertiesXmlFile() throws Exception { } @Test - public void testWithLocalProperties() throws Exception { + void testWithLocalProperties() throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); Properties localProps = new Properties(); localProps.setProperty("key2", "value2"); @@ -68,7 +68,7 @@ public void testWithLocalProperties() throws Exception { } @Test - public void testWithPropertiesFileAndLocalProperties() throws Exception { + void testWithPropertiesFileAndLocalProperties() throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); pfb.setLocation(TEST_PROPS); Properties localProps = new Properties(); @@ -82,7 +82,7 @@ public void testWithPropertiesFileAndLocalProperties() throws Exception { } @Test - public void testWithPropertiesFileAndMultipleLocalProperties() throws Exception { + void testWithPropertiesFileAndMultipleLocalProperties() throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); pfb.setLocation(TEST_PROPS); @@ -111,7 +111,7 @@ public void testWithPropertiesFileAndMultipleLocalProperties() throws Exception } @Test - public void testWithPropertiesFileAndLocalPropertiesAndLocalOverride() throws Exception { + void testWithPropertiesFileAndLocalPropertiesAndLocalOverride() throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); pfb.setLocation(TEST_PROPS); Properties localProps = new Properties(); @@ -126,7 +126,7 @@ public void testWithPropertiesFileAndLocalPropertiesAndLocalOverride() throws Ex } @Test - public void testWithPrototype() throws Exception { + void testWithPrototype() throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); pfb.setSingleton(false); pfb.setLocation(TEST_PROPS); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests.java index b406e3c93801..1f5432845a16 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,19 +28,19 @@ import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; /** - * Unit tests for {@link PropertyPathFactoryBean}. + * Tests for {@link PropertyPathFactoryBean}. * * @author Juergen Hoeller * @author Chris Beams * @since 04.10.2004 */ -public class PropertyPathFactoryBeanTests { +class PropertyPathFactoryBeanTests { private static final Resource CONTEXT = qualifiedResource(PropertyPathFactoryBeanTests.class, "context.xml"); @Test - public void testPropertyPathFactoryBeanWithSingletonResult() { + void testPropertyPathFactoryBeanWithSingletonResult() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); assertThat(xbf.getBean("propertyPath1")).isEqualTo(12); @@ -49,13 +49,13 @@ public void testPropertyPathFactoryBeanWithSingletonResult() { assertThat(xbf.getType("otb.spouse")).isEqualTo(ITestBean.class); Object result1 = xbf.getBean("otb.spouse"); Object result2 = xbf.getBean("otb.spouse"); - assertThat(result1 instanceof TestBean).isTrue(); + assertThat(result1).isInstanceOf(TestBean.class); assertThat(result1).isSameAs(result2); assertThat(((TestBean) result1).getAge()).isEqualTo(99); } @Test - public void testPropertyPathFactoryBeanWithPrototypeResult() { + void testPropertyPathFactoryBeanWithPrototypeResult() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); assertThat(xbf.getType("tb.spouse")).isNull(); @@ -63,9 +63,9 @@ public void testPropertyPathFactoryBeanWithPrototypeResult() { Object result1 = xbf.getBean("tb.spouse"); Object result2 = xbf.getBean("propertyPath3"); Object result3 = xbf.getBean("propertyPath3"); - assertThat(result1 instanceof TestBean).isTrue(); - assertThat(result2 instanceof TestBean).isTrue(); - assertThat(result3 instanceof TestBean).isTrue(); + assertThat(result1).isInstanceOf(TestBean.class); + assertThat(result2).isInstanceOf(TestBean.class); + assertThat(result3).isInstanceOf(TestBean.class); assertThat(((TestBean) result1).getAge()).isEqualTo(11); assertThat(((TestBean) result2).getAge()).isEqualTo(11); assertThat(((TestBean) result3).getAge()).isEqualTo(11); @@ -75,7 +75,7 @@ public void testPropertyPathFactoryBeanWithPrototypeResult() { } @Test - public void testPropertyPathFactoryBeanWithNullResult() { + void testPropertyPathFactoryBeanWithNullResult() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); assertThat(xbf.getType("tb.spouse.spouse")).isNull(); @@ -83,25 +83,25 @@ public void testPropertyPathFactoryBeanWithNullResult() { } @Test - public void testPropertyPathFactoryBeanAsInnerBean() { + void testPropertyPathFactoryBeanAsInnerBean() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); TestBean spouse = (TestBean) xbf.getBean("otb.spouse"); TestBean tbWithInner = (TestBean) xbf.getBean("tbWithInner"); assertThat(tbWithInner.getSpouse()).isSameAs(spouse); - assertThat(!tbWithInner.getFriends().isEmpty()).isTrue(); + assertThat(tbWithInner.getFriends()).isNotEmpty(); assertThat(tbWithInner.getFriends().iterator().next()).isSameAs(spouse); } @Test - public void testPropertyPathFactoryBeanAsNullReference() { + void testPropertyPathFactoryBeanAsNullReference() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); assertThat(xbf.getBean("tbWithNullReference", TestBean.class).getSpouse()).isNull(); } @Test - public void testPropertyPathFactoryBeanAsInnerNull() { + void testPropertyPathFactoryBeanAsInnerNull() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); assertThat(xbf.getBean("tbWithInnerNull", TestBean.class).getSpouse()).isNull(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java index 13d8b138ff43..01d387a95aba 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,10 @@ package org.springframework.beans.factory.config; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Properties; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -27,56 +30,54 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.registerWithGeneratedName; /** - * Unit tests for {@link PropertyPlaceholderConfigurer}. + * Tests for {@link PropertyPlaceholderConfigurer}. * * @author Chris Beams + * @author Sam Brannen */ @SuppressWarnings("deprecation") -public class PropertyPlaceholderConfigurerTests { +class PropertyPlaceholderConfigurerTests { private static final String P1 = "p1"; private static final String P1_LOCAL_PROPS_VAL = "p1LocalPropsVal"; private static final String P1_SYSTEM_PROPS_VAL = "p1SystemPropsVal"; - private DefaultListableBeanFactory bf; - private PropertyPlaceholderConfigurer ppc; - private Properties ppcProperties; + private final DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - private AbstractBeanDefinition p1BeanDef; + private final PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + private final Properties ppcProperties = new Properties(); - @BeforeEach - public void setup() { - p1BeanDef = rootBeanDefinition(TestBean.class) - .addPropertyValue("name", "${" + P1 + "}") - .getBeanDefinition(); + private AbstractBeanDefinition p1BeanDef = rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${" + P1 + "}") + .getBeanDefinition(); - bf = new DefaultListableBeanFactory(); - ppcProperties = new Properties(); + @BeforeEach + void setup() { ppcProperties.setProperty(P1, P1_LOCAL_PROPS_VAL); System.setProperty(P1, P1_SYSTEM_PROPS_VAL); - ppc = new PropertyPlaceholderConfigurer(); ppc.setProperties(ppcProperties); - } @AfterEach - public void cleanup() { + void cleanup() { System.clearProperty(P1); + System.clearProperty(P1_SYSTEM_PROPS_VAL); } @Test - public void localPropertiesViaResource() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void localPropertiesViaResource() { bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${my.name}") @@ -89,7 +90,7 @@ public void localPropertiesViaResource() { } @Test - public void resolveFromSystemProperties() { + void resolveFromSystemProperties() { System.setProperty("otherKey", "systemValue"); p1BeanDef = rootBeanDefinition(TestBean.class) .addPropertyValue("name", "${" + P1 + "}") @@ -104,7 +105,7 @@ public void resolveFromSystemProperties() { } @Test - public void resolveFromLocalProperties() { + void resolveFromLocalProperties() { System.clearProperty(P1); registerWithGeneratedName(p1BeanDef, bf); ppc.postProcessBeanFactory(bf); @@ -113,7 +114,7 @@ public void resolveFromLocalProperties() { } @Test - public void setSystemPropertiesMode_defaultIsFallback() { + void setSystemPropertiesMode_defaultIsFallback() { registerWithGeneratedName(p1BeanDef, bf); ppc.postProcessBeanFactory(bf); TestBean bean = bf.getBean(TestBean.class); @@ -121,7 +122,7 @@ public void setSystemPropertiesMode_defaultIsFallback() { } @Test - public void setSystemSystemPropertiesMode_toOverride_andResolveFromSystemProperties() { + void setSystemSystemPropertiesMode_toOverride_andResolveFromSystemProperties() { registerWithGeneratedName(p1BeanDef, bf); ppc.setSystemPropertiesMode(PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE); ppc.postProcessBeanFactory(bf); @@ -130,7 +131,7 @@ public void setSystemSystemPropertiesMode_toOverride_andResolveFromSystemPropert } @Test - public void setSystemSystemPropertiesMode_toOverride_andSetSearchSystemEnvironment_toFalse() { + void setSystemSystemPropertiesMode_toOverride_andSetSearchSystemEnvironment_toFalse() { registerWithGeneratedName(p1BeanDef, bf); System.clearProperty(P1); // will now fall all the way back to system environment ppc.setSearchSystemEnvironment(false); @@ -145,7 +146,7 @@ public void setSystemSystemPropertiesMode_toOverride_andSetSearchSystemEnvironme * settings regarding resolving properties from the environment. */ @Test - public void twoPlaceholderConfigurers_withConflictingSettings() { + void twoPlaceholderConfigurers_withConflictingSettings() { String P2 = "p2"; String P2_LOCAL_PROPS_VAL = "p2LocalPropsVal"; String P2_SYSTEM_PROPS_VAL = "p2SystemPropsVal"; @@ -184,12 +185,10 @@ public void twoPlaceholderConfigurers_withConflictingSettings() { } @Test - public void customPlaceholderPrefixAndSuffix() { - PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + void customPlaceholderPrefixAndSuffix() { ppc.setPlaceholderPrefix("@<"); ppc.setPlaceholderSuffix(">"); - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) .addPropertyValue("name", "@") @@ -207,11 +206,9 @@ public void customPlaceholderPrefixAndSuffix() { } @Test - public void nullValueIsPreserved() { - PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + void nullValueIsPreserved() { ppc.setNullValue("customNull"); System.setProperty("my.name", "customNull"); - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) .addPropertyValue("name", "${my.name}") .getBeanDefinition()); @@ -221,10 +218,8 @@ public void nullValueIsPreserved() { } @Test - public void trimValuesIsOffByDefault() { - PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + void trimValuesIsOffByDefault() { System.setProperty("my.name", " myValue "); - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) .addPropertyValue("name", "${my.name}") .getBeanDefinition()); @@ -234,11 +229,9 @@ public void trimValuesIsOffByDefault() { } @Test - public void trimValuesIsApplied() { - PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + void trimValuesIsApplied() { ppc.setTrimValues(true); System.setProperty("my.name", " myValue "); - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) .addPropertyValue("name", "${my.name}") .getBeanDefinition()); @@ -247,4 +240,23 @@ public void trimValuesIsApplied() { System.clearProperty("my.name"); } + /** + * This test effectively verifies that the internal 'constants' map is properly + * configured for all SYSTEM_PROPERTIES_MODE_ constants defined in + * {@link PropertyPlaceholderConfigurer}. + */ + @Test + @SuppressWarnings("deprecation") + void setSystemPropertiesModeNameToAllSupportedValues() { + streamSystemPropertiesModeConstants() + .map(Field::getName) + .forEach(name -> assertThatNoException().as(name).isThrownBy(() -> ppc.setSystemPropertiesModeName(name))); + } + + private static Stream streamSystemPropertiesModeConstants() { + return Arrays.stream(PropertyPlaceholderConfigurer.class.getFields()) + .filter(ReflectionUtils::isPublicStaticFinal) + .filter(field -> field.getName().startsWith("SYSTEM_PROPERTIES_MODE_")); + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java index ffbb92e8ba30..5c764fa87c53 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import java.util.Properties; import java.util.Set; import java.util.prefs.AbstractPreferences; -import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import java.util.prefs.PreferencesFactory; @@ -50,7 +49,7 @@ import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; /** - * Unit tests for various {@link PropertyResourceConfigurer} implementations including: + * Tests for various {@link PropertyResourceConfigurer} implementations including: * {@link PropertyPlaceholderConfigurer}, {@link PropertyOverrideConfigurer} and * {@link PreferencesPlaceholderConfigurer}. * @@ -61,7 +60,7 @@ * @see PropertyPlaceholderConfigurerTests */ @SuppressWarnings("deprecation") -public class PropertyResourceConfigurerTests { +class PropertyResourceConfigurerTests { static { System.setProperty("java.util.prefs.PreferencesFactory", MockPreferencesFactory.class.getName()); @@ -76,7 +75,7 @@ public class PropertyResourceConfigurerTests { @Test - public void testPropertyOverrideConfigurer() { + void testPropertyOverrideConfigurer() { BeanDefinition def1 = BeanDefinitionBuilder.genericBeanDefinition(TestBean.class).getBeanDefinition(); factory.registerBeanDefinition("tb1", def1); @@ -116,7 +115,7 @@ public void testPropertyOverrideConfigurer() { } @Test - public void testPropertyOverrideConfigurerWithNestedProperty() { + void testPropertyOverrideConfigurerWithNestedProperty() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); factory.registerBeanDefinition("tb", def); @@ -134,7 +133,7 @@ public void testPropertyOverrideConfigurerWithNestedProperty() { } @Test - public void testPropertyOverrideConfigurerWithNestedPropertyAndDotInBeanName() { + void testPropertyOverrideConfigurerWithNestedPropertyAndDotInBeanName() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); factory.registerBeanDefinition("my.tb", def); @@ -153,7 +152,7 @@ public void testPropertyOverrideConfigurerWithNestedPropertyAndDotInBeanName() { } @Test - public void testPropertyOverrideConfigurerWithNestedMapPropertyAndDotInMapKey() { + void testPropertyOverrideConfigurerWithNestedMapPropertyAndDotInMapKey() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); factory.registerBeanDefinition("tb", def); @@ -171,7 +170,7 @@ public void testPropertyOverrideConfigurerWithNestedMapPropertyAndDotInMapKey() } @Test - public void testPropertyOverrideConfigurerWithHeldProperties() { + void testPropertyOverrideConfigurerWithHeldProperties() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(PropertiesHolder.class).getBeanDefinition(); factory.registerBeanDefinition("tb", def); @@ -187,7 +186,7 @@ public void testPropertyOverrideConfigurerWithHeldProperties() { } @Test - public void testPropertyOverrideConfigurerWithPropertiesFile() { + void testPropertyOverrideConfigurerWithPropertiesFile() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); factory.registerBeanDefinition("tb", def); @@ -201,7 +200,7 @@ public void testPropertyOverrideConfigurerWithPropertiesFile() { } @Test - public void testPropertyOverrideConfigurerWithInvalidPropertiesFile() { + void testPropertyOverrideConfigurerWithInvalidPropertiesFile() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); factory.registerBeanDefinition("tb", def); @@ -216,7 +215,7 @@ public void testPropertyOverrideConfigurerWithInvalidPropertiesFile() { } @Test - public void testPropertyOverrideConfigurerWithPropertiesXmlFile() { + void testPropertyOverrideConfigurerWithPropertiesXmlFile() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); factory.registerBeanDefinition("tb", def); @@ -230,7 +229,7 @@ public void testPropertyOverrideConfigurerWithPropertiesXmlFile() { } @Test - public void testPropertyOverrideConfigurerWithConvertProperties() { + void testPropertyOverrideConfigurerWithConvertProperties() { BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); factory.registerBeanDefinition("tb", def); @@ -247,7 +246,7 @@ public void testPropertyOverrideConfigurerWithConvertProperties() { } @Test - public void testPropertyOverrideConfigurerWithInvalidKey() { + void testPropertyOverrideConfigurerWithInvalidKey() { factory.registerBeanDefinition("tb1", genericBeanDefinition(TestBean.class).getBeanDefinition()); factory.registerBeanDefinition("tb2", genericBeanDefinition(TestBean.class).getBeanDefinition()); @@ -282,7 +281,7 @@ public void testPropertyOverrideConfigurerWithInvalidKey() { } @Test - public void testPropertyOverrideConfigurerWithIgnoreInvalidKeys() { + void testPropertyOverrideConfigurerWithIgnoreInvalidKeys() { factory.registerBeanDefinition("tb1", genericBeanDefinition(TestBean.class).getBeanDefinition()); factory.registerBeanDefinition("tb2", genericBeanDefinition(TestBean.class).getBeanDefinition()); @@ -315,12 +314,12 @@ public void testPropertyOverrideConfigurerWithIgnoreInvalidKeys() { } @Test - public void testPropertyPlaceholderConfigurer() { + void testPropertyPlaceholderConfigurer() { doTestPropertyPlaceholderConfigurer(false); } @Test - public void testPropertyPlaceholderConfigurerWithParentChildSeparation() { + void testPropertyPlaceholderConfigurerWithParentChildSeparation() { doTestPropertyPlaceholderConfigurer(true); } @@ -401,12 +400,11 @@ private void doTestPropertyPlaceholderConfigurer(boolean parentChildSeparation) assertThat(tb1.getSpouse()).isEqualTo(tb2); assertThat(tb1.getSomeMap()).hasSize(1); assertThat(tb1.getSomeMap().get("myKey")).isEqualTo("myValue"); - assertThat(tb2.getStringArray()).hasSize(2); - assertThat(tb2.getStringArray()[0]).isEqualTo(System.getProperty("os.name")); - assertThat(tb2.getStringArray()[1]).isEqualTo("98"); - assertThat(tb2.getFriends()).hasSize(2); - assertThat(tb2.getFriends().iterator().next()).isEqualTo("na98me"); - assertThat(tb2.getFriends().toArray()[1]).isEqualTo(tb2); + assertThat(tb2.getStringArray()).containsExactly(System.getProperty("os.name"), "98"); + assertThat(tb2.getFriends()).satisfiesExactly( + zero -> assertThat(zero).isInstanceOfSatisfying( + String.class, value -> assertThat(value).isEqualTo("na98me")), + one -> assertThat(one).isEqualTo(tb2)); assertThat(tb2.getSomeSet()).hasSize(3); assertThat(tb2.getSomeSet().contains("na98me")).isTrue(); assertThat(tb2.getSomeSet().contains(tb2)).isTrue(); @@ -427,7 +425,7 @@ private void doTestPropertyPlaceholderConfigurer(boolean parentChildSeparation) } @Test - public void testPropertyPlaceholderConfigurerWithSystemPropertyFallback() { + void testPropertyPlaceholderConfigurerWithSystemPropertyFallback() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("country", "${os.name}").getBeanDefinition()); @@ -439,7 +437,7 @@ public void testPropertyPlaceholderConfigurerWithSystemPropertyFallback() { } @Test - public void testPropertyPlaceholderConfigurerWithSystemPropertyNotUsed() { + void testPropertyPlaceholderConfigurerWithSystemPropertyNotUsed() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("country", "${os.name}").getBeanDefinition()); @@ -454,7 +452,7 @@ public void testPropertyPlaceholderConfigurerWithSystemPropertyNotUsed() { } @Test - public void testPropertyPlaceholderConfigurerWithOverridingSystemProperty() { + void testPropertyPlaceholderConfigurerWithOverridingSystemProperty() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("country", "${os.name}").getBeanDefinition()); @@ -470,7 +468,7 @@ public void testPropertyPlaceholderConfigurerWithOverridingSystemProperty() { } @Test - public void testPropertyPlaceholderConfigurerWithUnresolvableSystemProperty() { + void testPropertyPlaceholderConfigurerWithUnresolvableSystemProperty() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("touchy", "${user.dir}").getBeanDefinition()); PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); @@ -481,7 +479,7 @@ public void testPropertyPlaceholderConfigurerWithUnresolvableSystemProperty() { } @Test - public void testPropertyPlaceholderConfigurerWithUnresolvablePlaceholder() { + void testPropertyPlaceholderConfigurerWithUnresolvablePlaceholder() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${ref}").getBeanDefinition()); PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); @@ -491,7 +489,7 @@ public void testPropertyPlaceholderConfigurerWithUnresolvablePlaceholder() { } @Test - public void testPropertyPlaceholderConfigurerWithIgnoreUnresolvablePlaceholder() { + void testPropertyPlaceholderConfigurerWithIgnoreUnresolvablePlaceholder() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${ref}").getBeanDefinition()); @@ -504,7 +502,7 @@ public void testPropertyPlaceholderConfigurerWithIgnoreUnresolvablePlaceholder() } @Test - public void testPropertyPlaceholderConfigurerWithEmptyStringAsNull() { + void testPropertyPlaceholderConfigurerWithEmptyStringAsNull() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "").getBeanDefinition()); @@ -517,7 +515,7 @@ public void testPropertyPlaceholderConfigurerWithEmptyStringAsNull() { } @Test - public void testPropertyPlaceholderConfigurerWithEmptyStringInPlaceholderAsNull() { + void testPropertyPlaceholderConfigurerWithEmptyStringInPlaceholderAsNull() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${ref}").getBeanDefinition()); @@ -533,7 +531,7 @@ public void testPropertyPlaceholderConfigurerWithEmptyStringInPlaceholderAsNull( } @Test - public void testPropertyPlaceholderConfigurerWithNestedPlaceholderInKey() { + void testPropertyPlaceholderConfigurerWithNestedPlaceholderInKey() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${my${key}key}").getBeanDefinition()); @@ -549,7 +547,7 @@ public void testPropertyPlaceholderConfigurerWithNestedPlaceholderInKey() { } @Test - public void testPropertyPlaceholderConfigurerWithPlaceholderInAlias() { + void testPropertyPlaceholderConfigurerWithPlaceholderInAlias() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class).getBeanDefinition()); factory.registerAlias("tb", "${alias}"); @@ -565,7 +563,7 @@ public void testPropertyPlaceholderConfigurerWithPlaceholderInAlias() { } @Test - public void testPropertyPlaceholderConfigurerWithSelfReferencingPlaceholderInAlias() { + void testPropertyPlaceholderConfigurerWithSelfReferencingPlaceholderInAlias() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class).getBeanDefinition()); factory.registerAlias("tb", "${alias}"); @@ -581,7 +579,7 @@ public void testPropertyPlaceholderConfigurerWithSelfReferencingPlaceholderInAli } @Test - public void testPropertyPlaceholderConfigurerWithCircularReference() { + void testPropertyPlaceholderConfigurerWithCircularReference() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("age", "${age}") .addPropertyValue("name", "name${var}") @@ -598,7 +596,7 @@ public void testPropertyPlaceholderConfigurerWithCircularReference() { } @Test - public void testPropertyPlaceholderConfigurerWithDefaultProperties() { + void testPropertyPlaceholderConfigurerWithDefaultProperties() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("touchy", "${test}").getBeanDefinition()); @@ -613,7 +611,7 @@ public void testPropertyPlaceholderConfigurerWithDefaultProperties() { } @Test - public void testPropertyPlaceholderConfigurerWithInlineDefault() { + void testPropertyPlaceholderConfigurerWithInlineDefault() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("touchy", "${test:mytest}").getBeanDefinition()); @@ -625,7 +623,7 @@ public void testPropertyPlaceholderConfigurerWithInlineDefault() { } @Test - public void testPropertyPlaceholderConfigurerWithAliases() { + void testPropertyPlaceholderConfigurerWithAliases() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("touchy", "${test}").getBeanDefinition()); @@ -649,7 +647,7 @@ public void testPropertyPlaceholderConfigurerWithAliases() { } @Test - public void testPreferencesPlaceholderConfigurer() { + void testPreferencesPlaceholderConfigurer() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${myName}") .addPropertyValue("age", "${myAge}") @@ -676,7 +674,7 @@ public void testPreferencesPlaceholderConfigurer() { } @Test - public void testPreferencesPlaceholderConfigurerWithCustomTreePaths() { + void testPreferencesPlaceholderConfigurerWithCustomTreePaths() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${myName}") .addPropertyValue("age", "${myAge}") @@ -705,7 +703,7 @@ public void testPreferencesPlaceholderConfigurerWithCustomTreePaths() { } @Test - public void testPreferencesPlaceholderConfigurerWithPathInPlaceholder() { + void testPreferencesPlaceholderConfigurerWithPathInPlaceholder() { factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) .addPropertyValue("name", "${mypath/myName}") .addPropertyValue("age", "${myAge}") @@ -812,16 +810,16 @@ protected void removeSpi(String key) { } @Override - protected void removeNodeSpi() throws BackingStoreException { + protected void removeNodeSpi() { } @Override - protected String[] keysSpi() throws BackingStoreException { + protected String[] keysSpi() { return StringUtils.toStringArray(values.keySet()); } @Override - protected String[] childrenNamesSpi() throws BackingStoreException { + protected String[] childrenNamesSpi() { return StringUtils.toStringArray(children.keySet()); } @@ -836,11 +834,11 @@ protected AbstractPreferences childSpi(String name) { } @Override - protected void syncSpi() throws BackingStoreException { + protected void syncSpi() { } @Override - protected void flushSpi() throws BackingStoreException { + protected void flushSpi() { } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java index 1d80dfd9b219..45956f2c1f0b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,23 +34,23 @@ import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; /** - * Unit tests for {@link ServiceLocatorFactoryBean}. + * Tests for {@link ServiceLocatorFactoryBean}. * * @author Colin Sampaleanu * @author Rick Evans * @author Chris Beams */ -public class ServiceLocatorFactoryBeanTests { +class ServiceLocatorFactoryBeanTests { private DefaultListableBeanFactory bf; @BeforeEach - public void setUp() { + void setUp() { bf = new DefaultListableBeanFactory(); } @Test - public void testNoArgGetter() { + void testNoArgGetter() { bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); bf.registerBeanDefinition("factory", genericBeanDefinition(ServiceLocatorFactoryBean.class) @@ -63,7 +63,7 @@ public void testNoArgGetter() { } @Test - public void testErrorOnTooManyOrTooFew() throws Exception { + void testErrorOnTooManyOrTooFew() { bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); bf.registerBeanDefinition("testServiceInstance2", genericBeanDefinition(TestService.class).getBeanDefinition()); bf.registerBeanDefinition("factory", @@ -87,7 +87,7 @@ public void testErrorOnTooManyOrTooFew() throws Exception { } @Test - public void testErrorOnTooManyOrTooFewWithCustomServiceLocatorException() { + void testErrorOnTooManyOrTooFewWithCustomServiceLocatorException() { bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); bf.registerBeanDefinition("testServiceInstance2", genericBeanDefinition(TestService.class).getBeanDefinition()); bf.registerBeanDefinition("factory", @@ -116,7 +116,7 @@ public void testErrorOnTooManyOrTooFewWithCustomServiceLocatorException() { } @Test - public void testStringArgGetter() throws Exception { + void testStringArgGetter() throws Exception { bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); bf.registerBeanDefinition("factory", genericBeanDefinition(ServiceLocatorFactoryBean.class) @@ -198,20 +198,20 @@ public void testServiceMappings() { assertThat(testBean3).isNotSameAs(testBean2); assertThat(testBean4).isNotSameAs(testBean2); assertThat(testBean4).isNotSameAs(testBean3); - assertThat(testBean1 instanceof ExtendedTestService).isFalse(); - assertThat(testBean2 instanceof ExtendedTestService).isFalse(); - assertThat(testBean3 instanceof ExtendedTestService).isFalse(); - assertThat(testBean4 instanceof ExtendedTestService).isTrue(); + assertThat(testBean1).isNotInstanceOf(ExtendedTestService.class); + assertThat(testBean2).isNotInstanceOf(ExtendedTestService.class); + assertThat(testBean3).isNotInstanceOf(ExtendedTestService.class); + assertThat(testBean4).isInstanceOf(ExtendedTestService.class); } @Test - public void testNoServiceLocatorInterfaceSupplied() throws Exception { + void testNoServiceLocatorInterfaceSupplied() { assertThatIllegalArgumentException().isThrownBy( new ServiceLocatorFactoryBean()::afterPropertiesSet); } @Test - public void testWhenServiceLocatorInterfaceIsNotAnInterfaceType() throws Exception { + void testWhenServiceLocatorInterfaceIsNotAnInterfaceType() { ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); factory.setServiceLocatorInterface(getClass()); assertThatIllegalArgumentException().isThrownBy( @@ -220,7 +220,7 @@ public void testWhenServiceLocatorInterfaceIsNotAnInterfaceType() throws Excepti } @Test - public void testWhenServiceLocatorExceptionClassToExceptionTypeWithOnlyNoArgCtor() throws Exception { + void testWhenServiceLocatorExceptionClassToExceptionTypeWithOnlyNoArgCtor() { ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); assertThatIllegalArgumentException().isThrownBy(() -> factory.setServiceLocatorExceptionClass(ExceptionClassWithOnlyZeroArgCtor.class)); @@ -229,7 +229,7 @@ public void testWhenServiceLocatorExceptionClassToExceptionTypeWithOnlyNoArgCtor @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - public void testWhenServiceLocatorExceptionClassIsNotAnExceptionSubclass() throws Exception { + public void testWhenServiceLocatorExceptionClassIsNotAnExceptionSubclass() { ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); assertThatIllegalArgumentException().isThrownBy(() -> factory.setServiceLocatorExceptionClass((Class) getClass())); @@ -237,7 +237,7 @@ public void testWhenServiceLocatorExceptionClassIsNotAnExceptionSubclass() throw } @Test - public void testWhenServiceLocatorMethodCalledWithTooManyParameters() throws Exception { + void testWhenServiceLocatorMethodCalledWithTooManyParameters() { ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); factory.setServiceLocatorInterface(ServiceLocatorInterfaceWithExtraNonCompliantMethod.class); factory.afterPropertiesSet(); @@ -247,7 +247,7 @@ public void testWhenServiceLocatorMethodCalledWithTooManyParameters() throws Exc } @Test - public void testRequiresListableBeanFactoryAndChokesOnAnythingElse() throws Exception { + void testRequiresListableBeanFactoryAndChokesOnAnythingElse() { BeanFactory beanFactory = mock(); try { ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/SimpleScopeTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/SimpleScopeTests.java index 21d207257454..48889787608c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/SimpleScopeTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/SimpleScopeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,13 +37,13 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class SimpleScopeTests { +class SimpleScopeTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setup() { + void setup() { beanFactory = new DefaultListableBeanFactory(); Scope scope = new NoOpScope() { private int index; @@ -73,7 +73,7 @@ public Object get(String name, ObjectFactory objectFactory) { @Test - public void testCanGetScopedObject() { + void testCanGetScopedObject() { TestBean tb1 = (TestBean) beanFactory.getBean("usesScope"); TestBean tb2 = (TestBean) beanFactory.getBean("usesScope"); assertThat(tb2).isNotSameAs(tb1); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java index defb200e810d..1f9dcc7709d0 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,20 +38,20 @@ * @author Dave Syer * @author Juergen Hoeller */ -public class YamlMapFactoryBeanTests { +class YamlMapFactoryBeanTests { private final YamlMapFactoryBean factory = new YamlMapFactoryBean(); @Test - public void testSetIgnoreResourceNotFound() { + void testSetIgnoreResourceNotFound() { this.factory.setResolutionMethod(YamlMapFactoryBean.ResolutionMethod.OVERRIDE_AND_IGNORE); this.factory.setResources(new FileSystemResource("non-exsitent-file.yml")); assertThat(this.factory.getObject()).isEmpty(); } @Test - public void testSetBarfOnResourceNotFound() { + void testSetBarfOnResourceNotFound() { assertThatIllegalStateException().isThrownBy(() -> { this.factory.setResources(new FileSystemResource("non-exsitent-file.yml")); this.factory.getObject().size(); @@ -59,14 +59,14 @@ public void testSetBarfOnResourceNotFound() { } @Test - public void testGetObject() { + void testGetObject() { this.factory.setResources(new ByteArrayResource("foo: bar".getBytes())); assertThat(this.factory.getObject()).hasSize(1); } @SuppressWarnings("unchecked") @Test - public void testOverrideAndRemoveDefaults() { + void testOverrideAndRemoveDefaults() { this.factory.setResources(new ByteArrayResource("foo:\n bar: spam".getBytes()), new ByteArrayResource("foo:\n spam: bar".getBytes())); @@ -75,7 +75,7 @@ public void testOverrideAndRemoveDefaults() { } @Test - public void testFirstFound() { + void testFirstFound() { this.factory.setResolutionMethod(YamlProcessor.ResolutionMethod.FIRST_FOUND); this.factory.setResources(new AbstractResource() { @Override @@ -92,14 +92,14 @@ public InputStream getInputStream() throws IOException { } @Test - public void testMapWithPeriodsInKey() { + void testMapWithPeriodsInKey() { this.factory.setResources(new ByteArrayResource("foo:\n ? key1.key2\n : value".getBytes())); Map map = this.factory.getObject(); assertThat(map).hasSize(1); assertThat(map.containsKey("foo")).isTrue(); Object object = map.get("foo"); - assertThat(object instanceof LinkedHashMap).isTrue(); + assertThat(object).isInstanceOf(LinkedHashMap.class); @SuppressWarnings("unchecked") Map sub = (Map) object; assertThat(sub.containsKey("key1.key2")).isTrue(); @@ -107,14 +107,14 @@ public void testMapWithPeriodsInKey() { } @Test - public void testMapWithIntegerValue() { + void testMapWithIntegerValue() { this.factory.setResources(new ByteArrayResource("foo:\n ? key1.key2\n : 3".getBytes())); Map map = this.factory.getObject(); assertThat(map).hasSize(1); assertThat(map.containsKey("foo")).isTrue(); Object object = map.get("foo"); - assertThat(object instanceof LinkedHashMap).isTrue(); + assertThat(object).isInstanceOf(LinkedHashMap.class); @SuppressWarnings("unchecked") Map sub = (Map) object; assertThat(sub).hasSize(1); @@ -122,7 +122,7 @@ public void testMapWithIntegerValue() { } @Test - public void testDuplicateKey() { + void testDuplicateKey() { this.factory.setResources(new ByteArrayResource("mymap:\n foo: bar\nmymap:\n bar: foo".getBytes())); assertThatExceptionOfType(DuplicateKeyException.class).isThrownBy(() -> this.factory.getObject().get("mymap")); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java index 5a14c49ac20b..7a039b11422a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.junit.jupiter.api.Test; -import org.yaml.snakeyaml.constructor.ConstructorException; +import org.yaml.snakeyaml.composer.ComposerException; import org.yaml.snakeyaml.parser.ParserException; import org.yaml.snakeyaml.scanner.ScannerException; @@ -33,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.InstanceOfAssertFactories.set; /** * Tests for {@link YamlProcessor}. @@ -141,14 +141,11 @@ void flattenedMapIsSameAsPropertiesButOrdered() { } @Test - @SuppressWarnings("unchecked") - void standardTypesSupportedByDefault() throws Exception { + void standardTypesSupportedByDefault() { setYaml("value: !!set\n ? first\n ? second"); this.processor.process((properties, map) -> { assertThat(properties).containsExactly(entry("value[0]", "first"), entry("value[1]", "second")); - assertThat(map.get("value")).isInstanceOf(Set.class); - Set set = (Set) map.get("value"); - assertThat(set).containsExactly("first", "second"); + assertThat(map.get("value")).asInstanceOf(set(String.class)).containsExactly("first", "second"); }); } @@ -156,9 +153,9 @@ void standardTypesSupportedByDefault() throws Exception { void customTypeNotSupportedByDefault() throws Exception { URL url = new URL("https://localhost:9000/"); setYaml("value: !!java.net.URL [\"" + url + "\"]"); - assertThatExceptionOfType(ConstructorException.class) + assertThatExceptionOfType(ComposerException.class) .isThrownBy(() -> this.processor.process((properties, map) -> {})) - .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); + .withMessageContaining("Global tag is not allowed: tag:yaml.org,2002:java.net.URL"); } @Test @@ -180,9 +177,9 @@ void customTypeNotSupportedDueToExplicitConfiguration() { setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]"); - assertThatExceptionOfType(ConstructorException.class) + assertThatExceptionOfType(ComposerException.class) .isThrownBy(() -> this.processor.process((properties, map) -> {})) - .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); + .withMessageContaining("Global tag is not allowed: tag:yaml.org,2002:java.net.URL"); } private void setYaml(String yaml) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntryTests.java index 9e3155dc4591..99baf84403c1 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,15 +21,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link ConstructorArgumentEntry}. + * Tests for {@link ConstructorArgumentEntry}. * * @author Rick Evans * @author Chris Beams */ -public class ConstructorArgumentEntryTests { +class ConstructorArgumentEntryTests { @Test - public void testCtorBailsOnNegativeCtorIndexArgument() { + void testCtorBailsOnNegativeCtorIndexArgument() { assertThatIllegalArgumentException().isThrownBy(() -> new ConstructorArgumentEntry(-1)); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/CustomProblemReporterTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/CustomProblemReporterTests.java index 82c8dd704eef..e5a9a9162bed 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/CustomProblemReporterTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/CustomProblemReporterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ * @author Chris Beams * @since 2.0 */ -public class CustomProblemReporterTests { +class CustomProblemReporterTests { private CollatingProblemReporter problemReporter; @@ -44,7 +44,7 @@ public class CustomProblemReporterTests { @BeforeEach - public void setup() { + void setup() { this.problemReporter = new CollatingProblemReporter(); this.beanFactory = new DefaultListableBeanFactory(); this.reader = new XmlBeanDefinitionReader(this.beanFactory); @@ -53,7 +53,7 @@ public void setup() { @Test - public void testErrorsAreCollated() { + void testErrorsAreCollated() { this.reader.loadBeanDefinitions(qualifiedResource(CustomProblemReporterTests.class, "context.xml")); assertThat(this.problemReporter.getErrors()).as("Incorrect number of errors collated").hasSize(4); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/FailFastProblemReporterTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/FailFastProblemReporterTests.java index 65527eb9b276..0f12880dc97b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/FailFastProblemReporterTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/FailFastProblemReporterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class FailFastProblemReporterTests { +class FailFastProblemReporterTests { @Test - public void testError() throws Exception { + void testError() { FailFastProblemReporter reporter = new FailFastProblemReporter(); assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> reporter.error(new Problem("VGER", new Location(new DescriptiveResource("here")), @@ -43,7 +43,7 @@ public void testError() throws Exception { } @Test - public void testWarn() throws Exception { + void testWarn() { Problem problem = new Problem("VGER", new Location(new DescriptiveResource("here")), null, new IllegalArgumentException()); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/NullSourceExtractorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/NullSourceExtractorTests.java index 48dd45999e0f..4f6972e38f67 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/NullSourceExtractorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/NullSourceExtractorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,17 +24,17 @@ * @author Rick Evans * @author Chris Beams */ -public class NullSourceExtractorTests { +class NullSourceExtractorTests { @Test - public void testPassThroughContract() throws Exception { - Object source = new Object(); + void testPassThroughContract() { + Object source = new Object(); Object extractedSource = new NullSourceExtractor().extractSource(source, null); assertThat(extractedSource).as("The contract of NullSourceExtractor states that the extraction *always* return null").isNull(); } @Test - public void testPassThroughContractEvenWithNull() throws Exception { + void testPassThroughContractEvenWithNull() { Object extractedSource = new NullSourceExtractor().extractSource(null, null); assertThat(extractedSource).as("The contract of NullSourceExtractor states that the extraction *always* return null").isNull(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ParseStateTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ParseStateTests.java index ce6a3aaa7df7..2a61a95fc56c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ParseStateTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ParseStateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,10 @@ * @author Chris Beams * @since 2.0 */ -public class ParseStateTests { +class ParseStateTests { @Test - public void testSimple() throws Exception { + void testSimple() { MockEntry entry = new MockEntry(); ParseState parseState = new ParseState(); @@ -39,7 +39,7 @@ public void testSimple() throws Exception { } @Test - public void testNesting() throws Exception { + void testNesting() { MockEntry one = new MockEntry(); MockEntry two = new MockEntry(); MockEntry three = new MockEntry(); @@ -59,7 +59,7 @@ public void testNesting() throws Exception { } @Test - public void testSnapshot() throws Exception { + void testSnapshot() { MockEntry entry = new MockEntry(); ParseState original = new ParseState(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractorTests.java index 2f6fe191d233..2c3770465dcf 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,23 +21,23 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link PassThroughSourceExtractor}. + * Tests for {@link PassThroughSourceExtractor}. * * @author Rick Evans * @author Chris Beams */ -public class PassThroughSourceExtractorTests { +class PassThroughSourceExtractorTests { @Test - public void testPassThroughContract() throws Exception { - Object source = new Object(); + void testPassThroughContract() { + Object source = new Object(); Object extractedSource = new PassThroughSourceExtractor().extractSource(source, null); assertThat(extractedSource).as("The contract of PassThroughSourceExtractor states that the supplied " + "source object *must* be returned as-is").isSameAs(source); } @Test - public void testPassThroughContractEvenWithNull() throws Exception { + void testPassThroughContractEvenWithNull() { Object extractedSource = new PassThroughSourceExtractor().extractSource(null, null); assertThat(extractedSource).as("The contract of PassThroughSourceExtractor states that the supplied " + "source object *must* be returned as-is (even if null)").isNull(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PropertyEntryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PropertyEntryTests.java index 084fb1f5a749..63a3a8be398f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PropertyEntryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PropertyEntryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,27 +21,27 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link PropertyEntry}. + * Tests for {@link PropertyEntry}. * * @author Rick Evans * @author Chris Beams */ -public class PropertyEntryTests { +class PropertyEntryTests { @Test - public void testCtorBailsOnNullPropertyNameArgument() throws Exception { + void testCtorBailsOnNullPropertyNameArgument() { assertThatIllegalArgumentException().isThrownBy(() -> new PropertyEntry(null)); } @Test - public void testCtorBailsOnEmptyPropertyNameArgument() throws Exception { + void testCtorBailsOnEmptyPropertyNameArgument() { assertThatIllegalArgumentException().isThrownBy(() -> new PropertyEntry("")); } @Test - public void testCtorBailsOnWhitespacedPropertyNameArgument() throws Exception { + void testCtorBailsOnWhitespacedPropertyNameArgument() { assertThatIllegalArgumentException().isThrownBy(() -> new PropertyEntry("\t ")); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/serviceloader/ServiceLoaderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/serviceloader/ServiceLoaderTests.java index 6f8f9568636a..111099a620dc 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/serviceloader/ServiceLoaderTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/serviceloader/ServiceLoaderTests.java @@ -48,7 +48,7 @@ void testServiceLoaderFactoryBean() { bd.getPropertyValues().add("serviceType", DocumentBuilderFactory.class.getName()); bf.registerBeanDefinition("service", bd); ServiceLoader serviceLoader = (ServiceLoader) bf.getBean("service"); - assertThat(serviceLoader.iterator().next() instanceof DocumentBuilderFactory).isTrue(); + assertThat(serviceLoader).element(0).isInstanceOf(DocumentBuilderFactory.class); } @Test @@ -57,7 +57,7 @@ void testServiceFactoryBean() { RootBeanDefinition bd = new RootBeanDefinition(ServiceFactoryBean.class); bd.getPropertyValues().add("serviceType", DocumentBuilderFactory.class.getName()); bf.registerBeanDefinition("service", bd); - assertThat(bf.getBean("service") instanceof DocumentBuilderFactory).isTrue(); + assertThat(bf.getBean("service")).isInstanceOf(DocumentBuilderFactory.class); } @Test @@ -67,7 +67,7 @@ void testServiceListFactoryBean() { bd.getPropertyValues().add("serviceType", DocumentBuilderFactory.class.getName()); bf.registerBeanDefinition("service", bd); List serviceList = (List) bf.getBean("service"); - assertThat(serviceList.get(0) instanceof DocumentBuilderFactory).isTrue(); + assertThat(serviceList).element(0).isInstanceOf(DocumentBuilderFactory.class); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java index e7e04d7390d8..8cb3f8beee7a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,16 +27,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link AutowireUtils}. + * Tests for {@link AutowireUtils}. * * @author Juergen Hoeller * @author Sam Brannen * @author Loïc Ledoyen */ -public class AutowireUtilsTests { +class AutowireUtilsTests { @Test - public void genericMethodReturnTypes() { + void genericMethodReturnTypes() { Method notParameterized = ReflectionUtils.findMethod(MyTypeWithMethods.class, "notParameterized"); Object actual = AutowireUtils.resolveReturnTypeForFactoryMethod(notParameterized, new Object[0], getClass().getClassLoader()); assertThat(actual).isEqualTo(String.class); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java index 5ef9a6dc8a29..63ebcc9caf92 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,82 +26,82 @@ /** * @author Juergen Hoeller */ -public class BeanDefinitionTests { +class BeanDefinitionTests { @Test - public void beanDefinitionEquality() { + void beanDefinitionEquality() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.setAbstract(true); bd.setLazyInit(true); bd.setScope("request"); RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.setAbstract(true); otherBd.setLazyInit(true); otherBd.setScope("request"); - assertThat(bd.equals(otherBd)).isTrue(); - assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd).isEqualTo(otherBd); + assertThat(otherBd).isEqualTo(bd); assertThat(bd.hashCode()).isEqualTo(otherBd.hashCode()); } @Test - public void beanDefinitionEqualityWithPropertyValues() { + void beanDefinitionEqualityWithPropertyValues() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.getPropertyValues().add("name", "myName"); bd.getPropertyValues().add("age", "99"); RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); otherBd.getPropertyValues().add("name", "myName"); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.getPropertyValues().add("age", "11"); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.getPropertyValues().add("age", "99"); - assertThat(bd.equals(otherBd)).isTrue(); - assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd).isEqualTo(otherBd); + assertThat(otherBd).isEqualTo(bd); assertThat(bd.hashCode()).isEqualTo(otherBd.hashCode()); } @Test - public void beanDefinitionEqualityWithConstructorArguments() { + void beanDefinitionEqualityWithConstructorArguments() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.getConstructorArgumentValues().addGenericArgumentValue("test"); bd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); otherBd.getConstructorArgumentValues().addGenericArgumentValue("test"); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 9); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); - assertThat(bd.equals(otherBd)).isTrue(); - assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd).isEqualTo(otherBd); + assertThat(otherBd).isEqualTo(bd); assertThat(bd.hashCode()).isEqualTo(otherBd.hashCode()); } @Test - public void beanDefinitionEqualityWithTypedConstructorArguments() { + void beanDefinitionEqualityWithTypedConstructorArguments() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.getConstructorArgumentValues().addGenericArgumentValue("test", "int"); bd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5, "long"); RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); otherBd.getConstructorArgumentValues().addGenericArgumentValue("test", "int"); otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5, "int"); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5, "long"); - assertThat(bd.equals(otherBd)).isTrue(); - assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd).isEqualTo(otherBd); + assertThat(otherBd).isEqualTo(bd); assertThat(bd.hashCode()).isEqualTo(otherBd.hashCode()); } @Test - public void genericBeanDefinitionEquality() { + void genericBeanDefinitionEquality() { GenericBeanDefinition bd = new GenericBeanDefinition(); bd.setParentName("parent"); bd.setScope("request"); @@ -111,45 +111,45 @@ public void genericBeanDefinitionEquality() { otherBd.setScope("request"); otherBd.setAbstract(true); otherBd.setLazyInit(true); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.setParentName("parent"); - assertThat(bd.equals(otherBd)).isTrue(); - assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd).isEqualTo(otherBd); + assertThat(otherBd).isEqualTo(bd); assertThat(bd.hashCode()).isEqualTo(otherBd.hashCode()); bd.getPropertyValues(); - assertThat(bd.equals(otherBd)).isTrue(); - assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd).isEqualTo(otherBd); + assertThat(otherBd).isEqualTo(bd); assertThat(bd.hashCode()).isEqualTo(otherBd.hashCode()); bd.getConstructorArgumentValues(); - assertThat(bd.equals(otherBd)).isTrue(); - assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd).isEqualTo(otherBd); + assertThat(otherBd).isEqualTo(bd); assertThat(bd.hashCode()).isEqualTo(otherBd.hashCode()); } @Test - public void beanDefinitionHolderEquality() { + void beanDefinitionHolderEquality() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.setAbstract(true); bd.setLazyInit(true); bd.setScope("request"); BeanDefinitionHolder holder = new BeanDefinitionHolder(bd, "bd"); RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); - assertThat(!bd.equals(otherBd)).isTrue(); - assertThat(!otherBd.equals(bd)).isTrue(); + assertThat(bd).isNotEqualTo(otherBd); + assertThat(otherBd).isNotEqualTo(bd); otherBd.setAbstract(true); otherBd.setLazyInit(true); otherBd.setScope("request"); BeanDefinitionHolder otherHolder = new BeanDefinitionHolder(bd, "bd"); - assertThat(holder.equals(otherHolder)).isTrue(); - assertThat(otherHolder.equals(holder)).isTrue(); + assertThat(holder).isEqualTo(otherHolder); + assertThat(otherHolder).isEqualTo(holder); assertThat(holder.hashCode()).isEqualTo(otherHolder.hashCode()); } @Test - public void beanDefinitionMerging() { + void beanDefinitionMerging() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); bd.getConstructorArgumentValues().addGenericArgumentValue("test"); bd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java index 0efebed42ac9..116e88587ef9 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,24 +16,25 @@ package org.springframework.beans.factory.support; -import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -import java.util.AbstractCollection; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.TypedStringValue; @@ -49,11 +50,10 @@ import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.UrlResource; -import org.springframework.core.testfixture.EnabledForTestGroups; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; +import static org.assertj.core.api.Assertions.entry; /** * @author Juergen Hoeller @@ -64,276 +64,241 @@ class BeanFactoryGenericsTests { @Test - void testGenericSetProperty() { + void genericSetProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - rbd.getPropertyValues().add("integerSet", input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.getPropertyValues().add("integerSet", Set.of("4", "5")); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericListProperty() throws Exception { + void genericListProperty() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - List input = new ArrayList<>(); - input.add("http://localhost:8080"); - input.add("http://localhost:9090"); - rbd.getPropertyValues().add("resourceList", input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + List input = List.of("http://localhost:8080", "http://localhost:9090"); + bd.getPropertyValues().add("resourceList", input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericListPropertyWithAutowiring() throws Exception { + void genericListPropertyWithAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); - RootBeanDefinition rbd = new RootBeanDefinition(GenericIntegerBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); - bf.registerBeanDefinition("genericBean", rbd); - GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericIntegerBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("genericBean"); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericListPropertyWithInvalidElementType() { + void genericListPropertyWithInvalidElementType() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericIntegerBean.class); - - List input = new ArrayList<>(); - input.add(1); - rbd.getPropertyValues().add("testBeanList", input); - - bf.registerBeanDefinition("genericBean", rbd); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - bf.getBean("genericBean")) - .withMessageContaining("genericBean") - .withMessageContaining("testBeanList[0]") - .withMessageContaining(TestBean.class.getName()) - .withMessageContaining("Integer"); + + RootBeanDefinition bd = new RootBeanDefinition(GenericIntegerBean.class); + bd.getPropertyValues().add("testBeanList", List.of(1)); + bf.registerBeanDefinition("genericBean", bd); + + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> bf.getBean("genericBean")) + .withMessageContaining("genericBean") + .withMessageContaining("testBeanList[0]") + .withMessageContaining(TestBean.class.getName()) + .withMessageContaining("Integer"); } @Test - void testGenericListPropertyWithOptionalAutowiring() { + void genericListPropertyWithOptionalAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getResourceList()).isNull(); } @Test - void testGenericMapProperty() { + void genericMapProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getPropertyValues().add("shortMap", input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getPropertyValues().add("shortMap", input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - void testGenericListOfArraysProperty() { + void genericListOfArraysProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - GenericBean gb = (GenericBean) bf.getBean("listOfArrays"); - assertThat(gb.getListOfArrays()).hasSize(1); - String[] array = gb.getListOfArrays().get(0); - assertThat(array).hasSize(2); - assertThat(array[0]).isEqualTo("value1"); - assertThat(array[1]).isEqualTo("value2"); + GenericBean gb = (GenericBean) bf.getBean("listOfArrays"); + assertThat(gb.getListOfArrays()).containsExactly(new String[] {"value1", "value2"}); } - @Test - void testGenericSetConstructor() { + void genericSetConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Set input = Set.of("4", "5"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericSetConstructorWithAutowiring() { + void genericSetConstructorWithAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("integer1", 4); bf.registerSingleton("integer2", 5); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericSetConstructorWithOptionalAutowiring() { + void genericSetConstructorWithOptionalAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getIntegerSet()).isNull(); } @Test - void testGenericSetListConstructor() throws Exception { + void genericSetListConstructor() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - List input2 = new ArrayList<>(); - input2.add("http://localhost:8080"); - input2.add("http://localhost:9090"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Set input1 = Set.of("4", "5"); + List input2 = List.of("http://localhost:8080", "http://localhost:9090"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericSetListConstructorWithAutowiring() throws Exception { + void genericSetListConstructorWithAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("integer1", 4); bf.registerSingleton("integer2", 5); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericSetListConstructorWithOptionalAutowiring() throws Exception { + void genericSetListConstructorWithOptionalAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getIntegerSet()).isNull(); assertThat(gb.getResourceList()).isNull(); } @Test - void testGenericSetMapConstructor() { + void genericSetMapConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Set input1 = Set.of("4", "5"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - void testGenericMapResourceConstructor() throws Exception { + void genericMapResourceConstructor() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList()).containsExactly(new UrlResource("http://localhost:8080")); } @Test - void testGenericMapMapConstructor() { + void genericMapMapConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Map input = new HashMap<>(); - input.put("1", "0"); - input.put("2", "3"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input1 = Map.of( + "1", "0", + "2", "3"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); assertThat(gb.getPlainMap()).hasSize(2); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); @@ -344,19 +309,18 @@ void testGenericMapMapConstructor() { } @Test - void testGenericMapMapConstructorWithSameRefAndConversion() { + void genericMapMapConstructorWithSameRefAndConversion() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("1", "0"); - input.put("2", "3"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "1", "0", + "2", "3"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); assertThat(gb.getPlainMap()).hasSize(2); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); @@ -367,19 +331,18 @@ void testGenericMapMapConstructorWithSameRefAndConversion() { } @Test - void testGenericMapMapConstructorWithSameRefAndNoConversion() { + void genericMapMapConstructorWithSameRefAndNoConversion() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); Map input = new HashMap<>(); input.put((short) 1, 0); input.put((short) 2, 3); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap()).isSameAs(gb.getPlainMap()); assertThat(gb.getShortMap()).hasSize(2); assertThat(gb.getShortMap().get(Short.valueOf("1"))).isEqualTo(0); @@ -387,150 +350,128 @@ void testGenericMapMapConstructorWithSameRefAndNoConversion() { } @Test - void testGenericMapWithKeyTypeConstructor() { + void genericMapWithKeyTypeConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getLongMap().get(4L)).isEqualTo("5"); assertThat(gb.getLongMap().get(6L)).isEqualTo("7"); } @Test - void testGenericMapWithCollectionValueConstructor() { + void genericMapWithCollectionValueConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addPropertyEditorRegistrar(registry -> registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Map> input = new HashMap<>(); - HashSet value1 = new HashSet<>(); - value1.add(1); - input.put("1", value1); - ArrayList value2 = new ArrayList<>(); - value2.add(Boolean.TRUE); - input.put("2", value2); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + bf.addPropertyEditorRegistrar(registry -> + registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); - assertThat(gb.getCollectionMap().get(1) instanceof HashSet).isTrue(); - assertThat(gb.getCollectionMap().get(2) instanceof ArrayList).isTrue(); - } + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map> input = Map.of( + "1", Set.of(1), + "2", List.of(Boolean.TRUE)); + bd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getCollectionMap().get(1)).isInstanceOf(Set.class); + assertThat(gb.getCollectionMap().get(2)).isInstanceOf(List.class); + } @Test - void testGenericSetFactoryMethod() { + void genericSetFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Set input = Set.of("4", "5"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericSetListFactoryMethod() throws Exception { + void genericSetListFactoryMethod() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - List input2 = new ArrayList<>(); - input2.add("http://localhost:8080"); - input2.add("http://localhost:9090"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Set input1 = Set.of("4", "5"); + List input2 = List.of("http://localhost:8080", "http://localhost:9090"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericSetMapFactoryMethod() { + void genericSetMapFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Set input1 = Set.of("4", "5"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - void testGenericMapResourceFactoryMethod() throws Exception { + void genericMapResourceFactoryMethod() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList()).containsExactly(new UrlResource("http://localhost:8080")); } @Test - void testGenericMapMapFactoryMethod() { + void genericMapMapFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Map input = new HashMap<>(); - input.put("1", "0"); - input.put("2", "3"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map input1 = Map.of( + "1", "0", + "2", "3"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); @@ -538,109 +479,104 @@ void testGenericMapMapFactoryMethod() { } @Test - void testGenericMapWithKeyTypeFactoryMethod() { + void genericMapWithKeyTypeFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getLongMap().get(Long.valueOf("4"))).isEqualTo("5"); assertThat(gb.getLongMap().get(Long.valueOf("6"))).isEqualTo("7"); } @Test - void testGenericMapWithCollectionValueFactoryMethod() { + void genericMapWithCollectionValueFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addPropertyEditorRegistrar(registry -> registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Map> input = new HashMap<>(); - HashSet value1 = new HashSet<>(); - value1.add(1); - input.put("1", value1); - ArrayList value2 = new ArrayList<>(); - value2.add(Boolean.TRUE); - input.put("2", value2); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + bf.addPropertyEditorRegistrar(registry -> + registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); + + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map> input = Map.of( + "1", Set.of(1), + "2", List.of(Boolean.TRUE)); + bd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getCollectionMap().get(1) instanceof HashSet).isTrue(); - assertThat(gb.getCollectionMap().get(2) instanceof ArrayList).isTrue(); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getCollectionMap().get(1)).isInstanceOf(Set.class); + assertThat(gb.getCollectionMap().get(2)).isInstanceOf(List.class); } @Test - void testGenericListBean() throws Exception { + void genericListBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - List list = (List) bf.getBean("list"); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo(new URL("http://localhost:8080")); + + NamedUrlList list = bf.getBean("list", NamedUrlList.class); + assertThat(list).containsExactly(new URL("http://localhost:8080")); } @Test - void testGenericSetBean() throws Exception { + void genericSetBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - Set set = (Set) bf.getBean("set"); - assertThat(set).hasSize(1); - assertThat(set.iterator().next()).isEqualTo(new URL("http://localhost:8080")); + + NamedUrlSet set = bf.getBean("set", NamedUrlSet.class); + assertThat(set).containsExactly(new URL("http://localhost:8080")); } @Test - void testGenericMapBean() throws Exception { + void genericMapBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - Map map = (Map) bf.getBean("map"); - assertThat(map).hasSize(1); - assertThat(map.keySet().iterator().next()).isEqualTo(10); - assertThat(map.values().iterator().next()).isEqualTo(new URL("http://localhost:8080")); + + NamedUrlMap map = bf.getBean("map", NamedUrlMap.class); + assertThat(map).containsExactly(entry(10, new URL("http://localhost:8080"))); } @Test - void testGenericallyTypedIntegerBean() { + void genericallyTypedIntegerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); + GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("integerBean"); assertThat(gb.getGenericProperty()).isEqualTo(10); - assertThat(gb.getGenericListProperty().get(0)).isEqualTo(20); - assertThat(gb.getGenericListProperty().get(1)).isEqualTo(30); + assertThat(gb.getGenericListProperty()).containsExactly(20, 30); } @Test - void testGenericallyTypedSetOfIntegerBean() { + void genericallyTypedSetOfIntegerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); + GenericSetOfIntegerBean gb = (GenericSetOfIntegerBean) bf.getBean("setOfIntegerBean"); - assertThat(gb.getGenericProperty().iterator().next()).isEqualTo(10); - assertThat(gb.getGenericListProperty().get(0).iterator().next()).isEqualTo(20); - assertThat(gb.getGenericListProperty().get(1).iterator().next()).isEqualTo(30); + assertThat(gb.getGenericProperty()).singleElement().isEqualTo(10); + assertThat(gb.getGenericListProperty()).satisfiesExactly( + zero -> assertThat(zero).containsExactly(20), + first -> assertThat(first).containsExactly(30)); } @Test - @EnabledForTestGroups(LONG_RUNNING) - void testSetBean() throws Exception { + void setBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - UrlSet us = (UrlSet) bf.getBean("setBean"); - assertThat(us).hasSize(1); - assertThat(us.iterator().next()).isEqualTo(new URL("https://www.springframework.org")); + + UrlSet urlSet = bf.getBean("setBean", UrlSet.class); + assertThat(urlSet).containsExactly(new URL("https://www.springframework.org")); } /** @@ -655,27 +591,27 @@ void testSetBean() throws Exception { */ @Test void parameterizedStaticFactoryMethod() { - RootBeanDefinition rbd = new RootBeanDefinition(getClass()); - rbd.setFactoryMethodName("createMockitoMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + RootBeanDefinition bd = new RootBeanDefinition(getClass()); + bd.setFactoryMethodName("createMockitoMock"); + bd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); - assertRunnableMockFactory(rbd); + assertRunnableMockFactory(bd); } @Test void parameterizedStaticFactoryMethodWithWrappedClassName() { - RootBeanDefinition rbd = new RootBeanDefinition(); - rbd.setBeanClassName(getClass().getName()); - rbd.setFactoryMethodName("createMockitoMock"); + RootBeanDefinition bd = new RootBeanDefinition(); + bd.setBeanClassName(getClass().getName()); + bd.setFactoryMethodName("createMockitoMock"); // TypedStringValue is used as an equivalent to an XML-defined argument String - rbd.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue(Runnable.class.getName())); + bd.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue(Runnable.class.getName())); - assertRunnableMockFactory(rbd); + assertRunnableMockFactory(bd); } - private void assertRunnableMockFactory(RootBeanDefinition rbd) { + private void assertRunnableMockFactory(RootBeanDefinition bd) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.registerBeanDefinition("mock", rbd); + bf.registerBeanDefinition("mock", bd); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); @@ -698,14 +634,14 @@ private void assertRunnableMockFactory(RootBeanDefinition rbd) { void parameterizedInstanceFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -719,14 +655,14 @@ void parameterizedInstanceFactoryMethod() { void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class.getName()); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class.getName()); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -740,14 +676,14 @@ void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { void parameterizedInstanceFactoryMethodWithInvalidClassName() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue("x"); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition rbd2 = new RootBeanDefinition(); + rbd2.setFactoryBeanName("mocksControl"); + rbd2.setFactoryMethodName("createMock"); + rbd2.getConstructorArgumentValues().addGenericArgumentValue("x"); + bf.registerBeanDefinition("mock", rbd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isFalse(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isFalse(); @@ -761,14 +697,14 @@ void parameterizedInstanceFactoryMethodWithInvalidClassName() { void parameterizedInstanceFactoryMethodWithIndexedArgument() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addIndexedArgumentValue(0, Runnable.class); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addIndexedArgumentValue(0, Runnable.class); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -783,14 +719,14 @@ void parameterizedInstanceFactoryMethodWithTempClassLoader() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setTempClassLoader(new OverridingClassLoader(getClass().getClassLoader())); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -801,7 +737,7 @@ void parameterizedInstanceFactoryMethodWithTempClassLoader() { } @Test - void testGenericMatchingWithBeanNameDifferentiation() { + void genericMatchingWithBeanNameDifferentiation() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); @@ -817,42 +753,44 @@ void testGenericMatchingWithBeanNameDifferentiation() { String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); - assertThat(numberStoreNames).hasSize(2); - assertThat(numberStoreNames[0]).isEqualTo("doubleStore"); - assertThat(numberStoreNames[1]).isEqualTo("floatStore"); + assertThat(numberStoreNames).containsExactly("doubleStore", "floatStore"); assertThat(doubleStoreNames).isEmpty(); assertThat(floatStoreNames).isEmpty(); } - @Test - void testGenericMatchingWithFullTypeDifferentiation() { + @ParameterizedTest + @ValueSource(classes = {NumberStoreFactory.class, NumberStoreFactoryBeans.class}) + void genericMatchingWithFullTypeDifferentiation(Class factoryClass) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); - RootBeanDefinition bd1 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd1 = new RootBeanDefinition(factoryClass); bd1.setFactoryMethodName("newDoubleStore"); bf.registerBeanDefinition("store1", bd1); - RootBeanDefinition bd2 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd2 = new RootBeanDefinition(factoryClass); bd2.setFactoryMethodName("newFloatStore"); bf.registerBeanDefinition("store2", bd2); - bf.registerBeanDefinition("numberBean", - new RootBeanDefinition(NumberBean.class, RootBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + RootBeanDefinition bd3 = new RootBeanDefinition(NumberBean.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + bd3.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("numberBean", bd3); + NumberStore store1 = bf.getBean("store1", NumberStore.class); + NumberStore store2 = bf.getBean("store2", NumberStore.class); NumberBean nb = bf.getBean(NumberBean.class); - assertThat(nb.getDoubleStore()).isSameAs(bf.getBean("store1")); - assertThat(nb.getFloatStore()).isSameAs(bf.getBean("store2")); + assertThat(nb.getDoubleStore()).isSameAs(store1); + assertThat(nb.getFloatStore()).isSameAs(store2); + nb = bf.getBean(NumberBean.class); + assertThat(nb.getDoubleStore()).isSameAs(store1); + assertThat(nb.getFloatStore()).isSameAs(store2); String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); - assertThat(numberStoreNames).hasSize(2); - assertThat(numberStoreNames[0]).isEqualTo("store1"); - assertThat(numberStoreNames[1]).isEqualTo("store2"); - assertThat(doubleStoreNames).hasSize(1); - assertThat(doubleStoreNames[0]).isEqualTo("store1"); - assertThat(floatStoreNames).hasSize(1); - assertThat(floatStoreNames[0]).isEqualTo("store2"); + assertThat(numberStoreNames).containsExactly("store1", "store2"); + assertThat(doubleStoreNames).containsExactly("store1"); + assertThat(floatStoreNames).containsExactly("store2"); ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); ObjectProvider> doubleStoreProvider = bf.getBeanProvider(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); @@ -860,80 +798,92 @@ void testGenericMatchingWithFullTypeDifferentiation() { assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getObject); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getIfAvailable); assertThat(numberStoreProvider.getIfUnique()).isNull(); - assertThat(doubleStoreProvider.getObject()).isSameAs(bf.getBean("store1")); - assertThat(doubleStoreProvider.getIfAvailable()).isSameAs(bf.getBean("store1")); - assertThat(doubleStoreProvider.getIfUnique()).isSameAs(bf.getBean("store1")); - assertThat(floatStoreProvider.getObject()).isSameAs(bf.getBean("store2")); - assertThat(floatStoreProvider.getIfAvailable()).isSameAs(bf.getBean("store2")); - assertThat(floatStoreProvider.getIfUnique()).isSameAs(bf.getBean("store2")); + assertThat(doubleStoreProvider.getObject()).isSameAs(store1); + assertThat(doubleStoreProvider.getIfAvailable()).isSameAs(store1); + assertThat(doubleStoreProvider.getIfUnique()).isSameAs(store1); + assertThat(floatStoreProvider.getObject()).isSameAs(store2); + assertThat(floatStoreProvider.getIfAvailable()).isSameAs(store2); + assertThat(floatStoreProvider.getIfUnique()).isSameAs(store2); List> resolved = new ArrayList<>(); for (NumberStore instance : numberStoreProvider) { resolved.add(instance); } - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); - - resolved = numberStoreProvider.stream().toList(); - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); - - resolved = numberStoreProvider.orderedStream().toList(); - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); + assertThat(resolved).containsExactly(store1, store2); + assertThat(numberStoreProvider.stream()).containsExactly(store1, store2); + assertThat(numberStoreProvider.orderedStream()).containsExactly(store2, store1); resolved = new ArrayList<>(); for (NumberStore instance : doubleStoreProvider) { resolved.add(instance); } - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); - - resolved = doubleStoreProvider.stream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); - - resolved = doubleStoreProvider.orderedStream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); + assertThat(resolved).containsExactly(store1); + assertThat(doubleStoreProvider.stream()).singleElement().isEqualTo(store1); + assertThat(doubleStoreProvider.orderedStream()).singleElement().isEqualTo(store1); resolved = new ArrayList<>(); for (NumberStore instance : floatStoreProvider) { resolved.add(instance); } - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); - - resolved = floatStoreProvider.stream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); - - resolved = floatStoreProvider.orderedStream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); + assertThat(resolved).containsExactly(store2); + assertThat(floatStoreProvider.stream()).singleElement().isEqualTo(store2); + assertThat(floatStoreProvider.orderedStream()).singleElement().isEqualTo(store2); } - @Test - void testGenericMatchingWithUnresolvedOrderedStream() { + @ParameterizedTest + @ValueSource(classes = {NumberStoreFactory.class, NumberStoreFactoryBeans.class}) + void genericMatchingWithUnresolvedOrderedStream(Class factoryClass) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); - RootBeanDefinition bd1 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd1 = new RootBeanDefinition(factoryClass); bd1.setFactoryMethodName("newDoubleStore"); bf.registerBeanDefinition("store1", bd1); - RootBeanDefinition bd2 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd2 = new RootBeanDefinition(factoryClass); bd2.setFactoryMethodName("newFloatStore"); bf.registerBeanDefinition("store2", bd2); ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); - List> resolved = numberStoreProvider.orderedStream().toList(); - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); + assertThat(numberStoreProvider.orderedStream()).containsExactly( + bf.getBean("store2", NumberStore.class), bf.getBean("store1", NumberStore.class)); + } + + @Test // gh-32489 + void genericMatchingAgainstFactoryBeanClass() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(MyFactoryBean.class); + bd.setTargetType(ResolvableType.forClassWithGenerics(MyFactoryBean.class, String.class)); + bf.registerBeanDefinition("myFactoryBean", bd); + bf.registerBeanDefinition("myFactoryBeanHolder", + new RootBeanDefinition(MyFactoryBeanHolder.class, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + + assertThat(bf.getBean(MyFactoryBeanHolder.class).factoryBeans).containsOnly(bf.getBean(MyFactoryBean.class)); + assertThat(bf.getBeanProvider(MyGenericInterfaceForFactoryBeans.class)).containsOnly(bf.getBean(MyFactoryBean.class)); + assertThat(bf.getBeanProvider(bd.getResolvableType())).containsOnly(bf.getBean(MyFactoryBean.class)); + } + + @Test // gh-32489 + void genericMatchingAgainstLazyFactoryBeanClass() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(MyFactoryBean.class); + // Replicate org.springframework.data.repository.config.RepositoryConfigurationDelegate#registerRepositoriesIn + // behavior of setting targetType, required to hit other branch in + // org.springframework.beans.factory.support.GenericTypeAwareAutowireCandidateResolver.checkGenericTypeMatch + bd.setTargetType(ResolvableType.forClassWithGenerics(MyFactoryBean.class, String.class)); + bd.setLazyInit(true); + bf.registerBeanDefinition("myFactoryBean", bd); + bf.registerBeanDefinition("myFactoryBeanHolder", + new RootBeanDefinition(MyFactoryBeanHolder.class, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + + assertThat(bf.getBeanProvider(bd.getResolvableType())).containsOnly(bf.getBean(MyFactoryBean.class)); + assertThat(bf.getBeanProvider(MyGenericInterfaceForFactoryBeans.class)).containsOnly(bf.getBean(MyFactoryBean.class)); + assertThat(bf.getBean(MyFactoryBeanHolder.class).factoryBeans).containsOnly(bf.getBean(MyFactoryBean.class)); + assertThat(bf.getBeanProvider(bd.getResolvableType())).containsOnly(bf.getBean(MyFactoryBean.class)); } @@ -999,7 +949,7 @@ public static class MocksControl { @SuppressWarnings("unchecked") public T createMock(Class toMock) { return (T) Proxy.newProxyInstance(BeanFactoryGenericsTests.class.getClassLoader(), new Class[] {toMock}, - (InvocationHandler) (proxy, method, args) -> { + (proxy, method, args) -> { throw new UnsupportedOperationException("mocked!"); }); } @@ -1052,4 +1002,64 @@ public static NumberStore newFloatStore() { } } + + public static class NumberStoreFactoryBeans { + + @Order(1) + public static FactoryBean> newDoubleStore() { + return new FactoryBean<>() { + @Override + public NumberStore getObject() { + return new DoubleStore(); + } + @Override + public Class getObjectType() { + return DoubleStore.class; + } + }; + } + + @Order(0) + public static FactoryBean> newFloatStore() { + return new FactoryBean<>() { + @Override + public NumberStore getObject() { + return new FloatStore(); + } + @Override + public Class getObjectType() { + return FloatStore.class; + } + }; + } + } + + + public interface MyGenericInterfaceForFactoryBeans { + } + + + public static class MyFactoryBean implements FactoryBean, MyGenericInterfaceForFactoryBeans { + + @Override + public T getObject() { + throw new UnsupportedOperationException(); + } + + @Override + public Class getObjectType() { + return String.class; + } + } + + + public static class MyFactoryBeanHolder { + + List> factoryBeans; // Requested type is not a FactoryBean type + + public MyFactoryBeanHolder(List> factoryBeans) { + this.factoryBeans = factoryBeans; + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java index d147b24d3502..c2e49654c57a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ * @author Phillip Webb * @author Juergen Hoeller */ -public class BeanFactorySupplierTests { +class BeanFactorySupplierTests { @Test void getBeanWhenUsingRegularSupplier() { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java index da6b27d3609e..144bc37564fd 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolder; import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolderFactoryBean; import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory; @@ -72,7 +73,7 @@ void detectBeanInstanceExecutableWithBeanClassNameAndFactoryMethodName() { } @Test - void beanDefinitionWithFactoryMethodNameAndAssignableConstructorArg() { + void beanDefinitionWithFactoryMethodNameAndAssignableIndexedConstructorArgs() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerSingleton("testNumber", 1L); beanFactory.registerSingleton("testBean", "test"); @@ -85,6 +86,34 @@ void beanDefinitionWithFactoryMethodNameAndAssignableConstructorArg() { .findMethod(SampleFactory.class, "create", Number.class, String.class)); } + @Test + void beanDefinitionWithFactoryMethodNameAndAssignableGenericConstructorArgs() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SampleFactory.class).setFactoryMethod("create") + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue("test"); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(1L); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(ReflectionUtils + .findMethod(SampleFactory.class, "create", Number.class, String.class)); + } + + @Test + void beanDefinitionWithFactoryMethodNameAndAssignableTypeStringValues() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SampleFactory.class).setFactoryMethod("create") + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues() + .addGenericArgumentValue(new TypedStringValue("test")); + beanDefinition.getConstructorArgumentValues() + .addGenericArgumentValue(new TypedStringValue("1", Integer.class)); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(ReflectionUtils + .findMethod(SampleFactory.class, "create", Number.class, String.class)); + } + @Test void beanDefinitionWithFactoryMethodNameAndMatchingMethodNames() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -122,7 +151,7 @@ void detectBeanInstanceExecutableWithBeanClassAndFactoryMethodNameIgnoreTargetTy } @Test - void beanDefinitionWithConstructorArgsForMultipleConstructors() throws Exception { + void beanDefinitionWithIndexedConstructorArgsForMultipleConstructors() throws Exception { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerSingleton("testNumber", 1L); beanFactory.registerSingleton("testBean", "test"); @@ -136,7 +165,22 @@ void beanDefinitionWithConstructorArgsForMultipleConstructors() throws Exception } @Test - void beanDefinitionWithMultiArgConstructorAndMatchingValue() throws NoSuchMethodException { + void beanDefinitionWithGenericConstructorArgsForMultipleConstructors() throws Exception { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("testNumber", 1L); + beanFactory.registerSingleton("testBean", "test"); + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(SampleBeanWithConstructors.class) + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue("test"); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(1L); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull().isEqualTo(SampleBeanWithConstructors.class + .getDeclaredConstructor(Number.class, String.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingIndexedValue() throws NoSuchMethodException { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(MultiConstructorSample.class) .addConstructorArgValue(42).getBeanDefinition(); @@ -146,7 +190,18 @@ void beanDefinitionWithMultiArgConstructorAndMatchingValue() throws NoSuchMethod } @Test - void beanDefinitionWithMultiArgConstructorAndMatchingArrayValue() throws NoSuchMethodException { + void beanDefinitionWithMultiArgConstructorAndMatchingGenericValue() throws NoSuchMethodException { + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(42); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorSample.class.getDeclaredConstructor(Integer.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingArrayFromIndexedValue() throws NoSuchMethodException { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(MultiConstructorArraySample.class) .addConstructorArgValue(42).getBeanDefinition(); @@ -156,7 +211,18 @@ void beanDefinitionWithMultiArgConstructorAndMatchingArrayValue() throws NoSuchM } @Test - void beanDefinitionWithMultiArgConstructorAndMatchingListValue() throws NoSuchMethodException { + void beanDefinitionWithMultiArgConstructorAndMatchingArrayFromGenericValue() throws NoSuchMethodException { + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorArraySample.class) + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(42); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo(MultiConstructorArraySample.class + .getDeclaredConstructor(Integer[].class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingListFromIndexedValue() throws NoSuchMethodException { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(MultiConstructorListSample.class) .addConstructorArgValue(42).getBeanDefinition(); @@ -166,7 +232,18 @@ void beanDefinitionWithMultiArgConstructorAndMatchingListValue() throws NoSuchMe } @Test - void beanDefinitionWithMultiArgConstructorAndMatchingValueAsInnerBean() throws NoSuchMethodException { + void beanDefinitionWithMultiArgConstructorAndMatchingListFromGenericValue() throws NoSuchMethodException { + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorListSample.class) + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(42); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorListSample.class.getDeclaredConstructor(List.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingIndexedValueAsInnerBean() throws NoSuchMethodException { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(MultiConstructorSample.class) .addConstructorArgValue( @@ -179,7 +256,20 @@ void beanDefinitionWithMultiArgConstructorAndMatchingValueAsInnerBean() throws N } @Test - void beanDefinitionWithMultiArgConstructorAndMatchingValueAsInnerBeanFactory() throws NoSuchMethodException { + void beanDefinitionWithMultiArgConstructorAndMatchingGenericValueAsInnerBean() throws NoSuchMethodException { + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue( + BeanDefinitionBuilder.rootBeanDefinition(Integer.class, "valueOf") + .addConstructorArgValue("42").getBeanDefinition()); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorSample.class.getDeclaredConstructor(Integer.class)); + } + + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingIndexedValueAsInnerBeanFactory() throws NoSuchMethodException { BeanDefinition beanDefinition = BeanDefinitionBuilder .rootBeanDefinition(MultiConstructorSample.class) .addConstructorArgValue(BeanDefinitionBuilder @@ -190,6 +280,18 @@ void beanDefinitionWithMultiArgConstructorAndMatchingValueAsInnerBeanFactory() t MultiConstructorSample.class.getDeclaredConstructor(Integer.class)); } + @Test + void beanDefinitionWithMultiArgConstructorAndMatchingGenericValueAsInnerBeanFactory() throws NoSuchMethodException { + AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSample.class) + .getBeanDefinition(); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue( + BeanDefinitionBuilder.rootBeanDefinition(IntegerFactoryBean.class).getBeanDefinition()); + Executable executable = resolve(new DefaultListableBeanFactory(), beanDefinition); + assertThat(executable).isNotNull().isEqualTo( + MultiConstructorSample.class.getDeclaredConstructor(Integer.class)); + } + @Test void beanDefinitionWithMultiArgConstructorAndNonMatchingValue() { BeanDefinition beanDefinition = BeanDefinitionBuilder @@ -312,6 +414,43 @@ void beanDefinitionWithClassArrayFactoryMethodArgAndAnotherMatchingConstructor() String[].class)); } + @Test + void beanDefinitionWithMultiConstructorSimilarArgumentsAndMatchingValues() throws NoSuchMethodException { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSimilarArgumentsSample.class) + .addConstructorArgValue("Test").addConstructorArgValue(1).addConstructorArgValue(2) + .getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull() + .isEqualTo(MultiConstructorSimilarArgumentsSample.class + .getDeclaredConstructor(String.class, Integer.class, Integer.class)); + } + + @Test + void beanDefinitionWithMultiConstructorSimilarArgumentsAndNullValueForCommonArgument() throws NoSuchMethodException { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSimilarArgumentsSample.class) + .addConstructorArgValue(null).addConstructorArgValue(null).addConstructorArgValue("Test") + .getBeanDefinition(); + Executable executable = resolve(beanFactory, beanDefinition); + assertThat(executable).isNotNull() + .isEqualTo(MultiConstructorSimilarArgumentsSample.class + .getDeclaredConstructor(String.class, Integer.class, String.class)); + } + + @Test + void beanDefinitionWithMultiConstructorSimilarArgumentsAndNullValueForSpecificArgument() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(MultiConstructorSimilarArgumentsSample.class) + .addConstructorArgValue(null).addConstructorArgValue(1).addConstructorArgValue(null) + .getBeanDefinition(); + assertThatIllegalStateException().isThrownBy(() -> resolve(beanFactory, beanDefinition)) + .withMessageContaining(MultiConstructorSimilarArgumentsSample.class.getName()); + } + @Test void beanDefinitionWithMultiArgConstructorAndPrimitiveConversion() throws NoSuchMethodException { BeanDefinition beanDefinition = BeanDefinitionBuilder @@ -432,6 +571,15 @@ static class MultiConstructorClassArraySample { } } + static class MultiConstructorSimilarArgumentsSample { + + MultiConstructorSimilarArgumentsSample(String name, Integer counter, String value) { + } + + MultiConstructorSimilarArgumentsSample(String name, Integer counter, Integer value) { + } + } + @SuppressWarnings("unused") static class ClassArrayFactoryMethodSample { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefinitionMetadataEqualsHashCodeTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefinitionMetadataEqualsHashCodeTests.java index 36c0c4be39cc..e6d9d851b17a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefinitionMetadataEqualsHashCodeTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefinitionMetadataEqualsHashCodeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,16 +25,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@code equals()} and {@code hashCode()} in bean definitions. + * Tests for {@code equals()} and {@code hashCode()} in bean definitions. * * @author Rob Harrop * @author Sam Brannen */ @SuppressWarnings("serial") -public class DefinitionMetadataEqualsHashCodeTests { +class DefinitionMetadataEqualsHashCodeTests { @Test - public void rootBeanDefinition() { + void rootBeanDefinition() { RootBeanDefinition master = new RootBeanDefinition(TestBean.class); RootBeanDefinition equal = new RootBeanDefinition(TestBean.class); RootBeanDefinition notEqual = new RootBeanDefinition(String.class); @@ -53,7 +53,7 @@ public void rootBeanDefinition() { * @see SPR-11420 */ @Test - public void rootBeanDefinitionAndMethodOverridesWithDifferentOverloadedValues() { + void rootBeanDefinitionAndMethodOverridesWithDifferentOverloadedValues() { RootBeanDefinition master = new RootBeanDefinition(TestBean.class); RootBeanDefinition equal = new RootBeanDefinition(TestBean.class); @@ -73,7 +73,7 @@ public void rootBeanDefinitionAndMethodOverridesWithDifferentOverloadedValues() } @Test - public void childBeanDefinition() { + void childBeanDefinition() { ChildBeanDefinition master = new ChildBeanDefinition("foo"); ChildBeanDefinition equal = new ChildBeanDefinition("foo"); ChildBeanDefinition notEqual = new ChildBeanDefinition("bar"); @@ -88,7 +88,7 @@ public void childBeanDefinition() { } @Test - public void runtimeBeanReference() { + void runtimeBeanReference() { RuntimeBeanReference master = new RuntimeBeanReference("name"); RuntimeBeanReference equal = new RuntimeBeanReference("name"); RuntimeBeanReference notEqual = new RuntimeBeanReference("someOtherName"); @@ -104,7 +104,7 @@ private void setBaseProperties(AbstractBeanDefinition definition) { definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); // definition.getConstructorArgumentValues().addGenericArgumentValue("foo"); definition.setDependencyCheck(AbstractBeanDefinition.DEPENDENCY_CHECK_OBJECTS); - definition.setDependsOn(new String[] { "foo", "bar" }); + definition.setDependsOn("foo", "bar"); definition.setDestroyMethodName("destroy"); definition.setEnforceDestroyMethod(false); definition.setEnforceInitMethod(true); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java index 414fd0157661..9e805c9408c5 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,13 @@ * @author Karl Pietrzak * @author Juergen Hoeller */ -public class LookupMethodTests { +class LookupMethodTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setup() { + void setup() { beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(new ClassPathResource("lookupMethodTests.xml", getClass())); @@ -44,7 +44,7 @@ public void setup() { @Test - public void testWithoutConstructorArg() { + void testWithoutConstructorArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); assertThat(bean).isNotNull(); Object expected = bean.get(); @@ -52,7 +52,7 @@ public void testWithoutConstructorArg() { } @Test - public void testWithOverloadedArg() { + void testWithOverloadedArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); assertThat(bean).isNotNull(); TestBean expected = bean.get("haha"); @@ -61,7 +61,7 @@ public void testWithOverloadedArg() { } @Test - public void testWithOneConstructorArg() { + void testWithOneConstructorArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); assertThat(bean).isNotNull(); TestBean expected = bean.getOneArgument("haha"); @@ -70,7 +70,7 @@ public void testWithOneConstructorArg() { } @Test - public void testWithTwoConstructorArg() { + void testWithTwoConstructorArg() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); assertThat(bean).isNotNull(); TestBean expected = bean.getTwoArguments("haha", 72); @@ -80,7 +80,7 @@ public void testWithTwoConstructorArg() { } @Test - public void testWithThreeArgsShouldFail() { + void testWithThreeArgsShouldFail() { AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); assertThat(bean).isNotNull(); assertThatExceptionOfType(AbstractMethodError.class).as("does not have a three arg constructor") @@ -88,7 +88,7 @@ public void testWithThreeArgsShouldFail() { } @Test - public void testWithOverriddenLookupMethod() { + void testWithOverriddenLookupMethod() { AbstractBean bean = (AbstractBean) beanFactory.getBean("extendedBean"); assertThat(bean).isNotNull(); TestBean expected = bean.getOneArgument("haha"); @@ -98,7 +98,7 @@ public void testWithOverriddenLookupMethod() { } @Test - public void testWithGenericBean() { + void testWithGenericBean() { RootBeanDefinition bd = new RootBeanDefinition(NumberBean.class); bd.getMethodOverrides().addOverride(new LookupOverride("getDoubleStore", null)); bd.getMethodOverrides().addOverride(new LookupOverride("getFloatStore", null)); @@ -113,7 +113,7 @@ public void testWithGenericBean() { } - public static abstract class AbstractBean { + public abstract static class AbstractBean { public abstract TestBean get(); @@ -139,7 +139,7 @@ public static class FloatStore extends NumberStore { } - public static abstract class NumberBean { + public abstract static class NumberBean { public abstract NumberStore getDoubleStore(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java index 9d8a1cbe36e0..35a4323f821e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for {@link ManagedList}. + * Tests for {@link ManagedList}. * * @author Rick Evans * @author Juergen Hoeller diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java index a7929bf30d9c..6f98e33cee49 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ * @author Sam Brannen */ @SuppressWarnings({ "rawtypes", "unchecked" }) -public class ManagedMapTests { +class ManagedMapTests { @Test - public void mergeSunnyDay() { + void mergeSunnyDay() { ManagedMap parent = ManagedMap.ofEntries(Map.entry("one", "one"), Map.entry("two", "two")); ManagedMap child = ManagedMap.ofEntries(Map.entry("tree", "three")); @@ -43,14 +43,14 @@ public void mergeSunnyDay() { } @Test - public void mergeWithNullParent() { + void mergeWithNullParent() { ManagedMap child = new ManagedMap(); child.setMergeEnabled(true); assertThat(child.merge(null)).isSameAs(child); } @Test - public void mergeWithNonCompatibleParentType() { + void mergeWithNonCompatibleParentType() { ManagedMap map = new ManagedMap(); map.setMergeEnabled(true); assertThatIllegalArgumentException().isThrownBy(() -> @@ -58,13 +58,13 @@ public void mergeWithNonCompatibleParentType() { } @Test - public void mergeNotAllowedWhenMergeNotEnabled() { + void mergeNotAllowedWhenMergeNotEnabled() { assertThatIllegalStateException().isThrownBy(() -> new ManagedMap().merge(null)); } @Test - public void mergeEmptyChild() { + void mergeEmptyChild() { ManagedMap parent = ManagedMap.ofEntries(Map.entry("one", "one"), Map.entry("two", "two")); ManagedMap child = new ManagedMap(); @@ -74,7 +74,7 @@ public void mergeEmptyChild() { } @Test - public void mergeChildValuesOverrideTheParents() { + void mergeChildValuesOverrideTheParents() { ManagedMap parent = ManagedMap.ofEntries(Map.entry("one", "one"), Map.entry("two", "two")); ManagedMap child = ManagedMap.ofEntries(Map.entry("one", "fork")); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedPropertiesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedPropertiesTests.java index 25739ac772a3..7a2efffda04f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedPropertiesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ * @author Sam Brannen */ @SuppressWarnings("rawtypes") -public class ManagedPropertiesTests { +class ManagedPropertiesTests { @Test @SuppressWarnings("unchecked") @@ -46,14 +46,14 @@ public void mergeSunnyDay() { } @Test - public void mergeWithNullParent() { + void mergeWithNullParent() { ManagedProperties child = new ManagedProperties(); child.setMergeEnabled(true); assertThat(child.merge(null)).isSameAs(child); } @Test - public void mergeWithNonCompatibleParentType() { + void mergeWithNonCompatibleParentType() { ManagedProperties map = new ManagedProperties(); map.setMergeEnabled(true); assertThatIllegalArgumentException().isThrownBy(() -> @@ -61,7 +61,7 @@ public void mergeWithNonCompatibleParentType() { } @Test - public void mergeNotAllowedWhenMergeNotEnabled() { + void mergeNotAllowedWhenMergeNotEnabled() { ManagedProperties map = new ManagedProperties(); assertThatIllegalStateException().isThrownBy(() -> map.merge(null)); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java index 01f330f1c5a7..274c63aad6a9 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for {@link ManagedSet}. + * Tests for {@link ManagedSet}. * * @author Rick Evans * @author Juergen Hoeller diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java index fe190c8b9f5c..f9ecb7e6c7d7 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ * @author Mark Fisher * @author Juergen Hoeller */ -public class QualifierAnnotationAutowireBeanFactoryTests { +class QualifierAnnotationAutowireBeanFactoryTests { private static final String JUERGEN = "juergen"; @@ -45,7 +45,7 @@ public class QualifierAnnotationAutowireBeanFactoryTests { @Test - public void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { + void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -59,7 +59,7 @@ public void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Except } @Test - public void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Exception { + void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -75,7 +75,7 @@ public void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throw @Disabled @Test - public void testAutowireCandidateWithFieldDescriptor() throws Exception { + void testAutowireCandidateWithFieldDescriptor() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -99,7 +99,7 @@ public void testAutowireCandidateWithFieldDescriptor() throws Exception { } @Test - public void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception { + void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -117,7 +117,7 @@ public void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exc } @Test - public void testAutowireCandidateWithShortClassName() throws Exception { + void testAutowireCandidateWithShortClassName() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -135,7 +135,7 @@ public void testAutowireCandidateWithShortClassName() throws Exception { @Disabled @Test - public void testAutowireCandidateWithConstructorDescriptor() throws Exception { + void testAutowireCandidateWithConstructorDescriptor() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -157,7 +157,7 @@ public void testAutowireCandidateWithConstructorDescriptor() throws Exception { @Disabled @Test - public void testAutowireCandidateWithMethodDescriptor() throws Exception { + void testAutowireCandidateWithMethodDescriptor() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -187,7 +187,7 @@ public void testAutowireCandidateWithMethodDescriptor() throws Exception { } @Test - public void testAutowireCandidateWithMultipleCandidatesDescriptor() throws Exception { + void testAutowireCandidateWithMultipleCandidatesDescriptor() throws Exception { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/SimpleInstantiationStrategyTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/SimpleInstantiationStrategyTests.java new file mode 100644 index 000000000000..a058356dfc9a --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/SimpleInstantiationStrategyTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link SimpleInstantiationStrategy}. + * + * @author Stephane Nicoll + */ +class SimpleInstantiationStrategyTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private final SimpleInstantiationStrategy strategy = new SimpleInstantiationStrategy(); + + @Test + void instantiateWithNoArg() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + Object simpleBean = instantiate(bd, new SampleFactory(), + method(SampleFactory.class, "simpleBean")); + assertThat(simpleBean).isEqualTo("Hello"); + } + + @Test + void instantiateWithArgs() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + Object simpleBean = instantiate(bd, new SampleFactory(), + method(SampleFactory.class, "beanWithTwoArgs"), "Test", 42); + assertThat(simpleBean).isEqualTo("Test42"); + } + + @Test + void instantiateWithSubClassFactoryArgs() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + Object simpleBean = instantiate(bd, new ExtendedSampleFactory(), + method(SampleFactory.class, "beanWithTwoArgs"), "Test", 42); + assertThat(simpleBean).isEqualTo("42Test"); + } + + @Test + void instantiateWithNullValueReturnsNullBean() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + Object simpleBean = instantiate(bd, new SampleFactory(), + method(SampleFactory.class, "cloneBean"), new Object[] { null }); + assertThat(simpleBean).isNotNull().isInstanceOf(NullBean.class); + } + + @Test + void instantiateWithArgumentTypeMismatch() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> instantiate( + bd, new SampleFactory(), + method(SampleFactory.class, "beanWithTwoArgs"), 42, "Test")) + .withMessageContaining("Illegal arguments to factory method 'beanWithTwoArgs'") + .withMessageContaining("args: 42,Test"); + } + + @Test + void instantiateWithTargetTypeMismatch() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> instantiate( + bd, new AnotherFactory(), + method(SampleFactory.class, "beanWithTwoArgs"), "Test", 42)) + .withMessageContaining("Illegal factory instance for factory method 'beanWithTwoArgs'") + .withMessageContaining("instance: " + AnotherFactory.class.getName()) + .withMessageNotContaining("args: Test,42"); + } + + @Test + void instantiateWithTargetTypeNotAssignable() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> instantiate( + bd, new SampleFactory(), + method(ExtendedSampleFactory.class, "beanWithTwoArgs"), "Test", 42)) + .withMessageContaining("Illegal factory instance for factory method 'beanWithTwoArgs'") + .withMessageContaining("instance: " + SampleFactory.class.getName()) + .withMessageNotContaining("args: Test,42"); + } + + @Test + void instantiateWithException() { + RootBeanDefinition bd = new RootBeanDefinition(String.class); + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> instantiate( + bd, new SampleFactory(), + method(SampleFactory.class, "errorBean"), "This a test message")) + .withMessageContaining("Factory method 'errorBean' threw exception") + .withMessageContaining("This a test message") + .havingCause().isInstanceOf(IllegalStateException.class).withMessage("This a test message"); + } + + private Object instantiate(RootBeanDefinition bd, Object factory, Method method, Object... args) { + return this.strategy.instantiate(bd, "simpleBean", this.beanFactory, + factory, method, args); + } + + private static Method method(Class target, String methodName) { + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods( + target, method -> methodName.equals(method.getName())); + assertThat(methods).as("No unique method named " + methodName + " found of " + target.getName()) + .hasSize(1); + return methods[0]; + } + + + static class SampleFactory { + + String simpleBean() { + return "Hello"; + } + + String beanWithTwoArgs(String first, Integer second) { + return first + second; + } + + String cloneBean(String arg) { + return arg; + } + + String errorBean(String msg) { + throw new IllegalStateException(msg); + } + + } + + static class ExtendedSampleFactory extends SampleFactory { + + @Override + String beanWithTwoArgs(String first, Integer second) { + return second + first; + } + } + + static class AnotherFactory { + + String beanWithTwoArgs(String first, Integer second) { + return second + first; + } + + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/Spr8954Tests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/Spr8954Tests.java index 19dfaa5974ae..37634d99ccaa 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/Spr8954Tests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/Spr8954Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ /** - * Unit tests for SPR-8954, in which a custom {@link InstantiationAwareBeanPostProcessor} + * Tests for SPR-8954, in which a custom {@link InstantiationAwareBeanPostProcessor} * forces the predicted type of a FactoryBean, effectively preventing retrieval of the * bean from calls to #getBeansOfType(FactoryBean.class). The implementation of * {@link AbstractBeanFactory#isFactoryBean(String, RootBeanDefinition)} now ensures that @@ -40,65 +40,60 @@ * @author Chris Beams * @author Oliver Gierke */ -public class Spr8954Tests { +class Spr8954Tests { private DefaultListableBeanFactory bf; @BeforeEach - public void setUp() { + void setUp() { bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("foo", new RootBeanDefinition(FooFactoryBean.class)); bf.addBeanPostProcessor(new PredictingBPP()); } @Test - public void repro() { + void repro() { assertThat(bf.getBean("foo")).isInstanceOf(Foo.class); assertThat(bf.getBean("&foo")).isInstanceOf(FooFactoryBean.class); assertThat(bf.isTypeMatch("&foo", FactoryBean.class)).isTrue(); @SuppressWarnings("rawtypes") Map fbBeans = bf.getBeansOfType(FactoryBean.class); - assertThat(fbBeans).hasSize(1); - assertThat(fbBeans.keySet()).contains("&foo"); + assertThat(fbBeans).containsOnlyKeys("&foo"); Map aiBeans = bf.getBeansOfType(AnInterface.class); - assertThat(aiBeans).hasSize(1); - assertThat(aiBeans.keySet()).contains("&foo"); + assertThat(aiBeans).containsOnlyKeys("&foo"); } @Test - public void findsBeansByTypeIfNotInstantiated() { + void findsBeansByTypeIfNotInstantiated() { assertThat(bf.isTypeMatch("&foo", FactoryBean.class)).isTrue(); @SuppressWarnings("rawtypes") Map fbBeans = bf.getBeansOfType(FactoryBean.class); - assertThat(fbBeans.size()).isEqualTo(1); - assertThat(fbBeans.keySet().iterator().next()).isEqualTo("&foo"); + assertThat(fbBeans).containsOnlyKeys("&foo"); Map aiBeans = bf.getBeansOfType(AnInterface.class); - assertThat(aiBeans).hasSize(1); - assertThat(aiBeans.keySet()).contains("&foo"); + assertThat(aiBeans).containsOnlyKeys("&foo"); } /** * SPR-10517 */ @Test - public void findsFactoryBeanNameByTypeWithoutInstantiation() { + void findsFactoryBeanNameByTypeWithoutInstantiation() { String[] names = bf.getBeanNamesForType(AnInterface.class, false, false); assertThat(Arrays.asList(names)).contains("&foo"); Map beans = bf.getBeansOfType(AnInterface.class, false, false); - assertThat(beans).hasSize(1); - assertThat(beans.keySet()).contains("&foo"); + assertThat(beans).containsOnlyKeys("&foo"); } static class FooFactoryBean implements FactoryBean, AnInterface { @Override - public Foo getObject() throws Exception { + public Foo getObject() { return new Foo(); } @@ -129,7 +124,7 @@ static class PredictingBPP implements SmartInstantiationAwareBeanPostProcessor { @Override public Class predictBeanType(Class beanClass, String beanName) { - return FactoryBean.class.isAssignableFrom(beanClass) ? PredictedType.class : null; + return (FactoryBean.class.isAssignableFrom(beanClass) ? PredictedType.class : null); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanConfigurerSupportTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanConfigurerSupportTests.java index daaeab1371b4..928779d2064a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanConfigurerSupportTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanConfigurerSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,16 +33,16 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class BeanConfigurerSupportTests { +class BeanConfigurerSupportTests { @Test - public void supplyIncompatibleBeanFactoryImplementation() { + void supplyIncompatibleBeanFactoryImplementation() { assertThatIllegalArgumentException().isThrownBy(() -> new StubBeanConfigurerSupport().setBeanFactory(mock())); } @Test - public void configureBeanDoesNothingIfBeanWiringInfoResolverResolvesToNull() throws Exception { + void configureBeanDoesNothingIfBeanWiringInfoResolverResolvesToNull() { TestBean beanInstance = new TestBean(); BeanWiringInfoResolver resolver = mock(); @@ -56,7 +56,7 @@ public void configureBeanDoesNothingIfBeanWiringInfoResolverResolvesToNull() thr } @Test - public void configureBeanDoesNothingIfNoBeanFactoryHasBeenSet() throws Exception { + void configureBeanDoesNothingIfNoBeanFactoryHasBeenSet() { TestBean beanInstance = new TestBean(); BeanConfigurerSupport configurer = new StubBeanConfigurerSupport(); configurer.configureBean(beanInstance); @@ -64,7 +64,7 @@ public void configureBeanDoesNothingIfNoBeanFactoryHasBeenSet() throws Exception } @Test - public void configureBeanReallyDoesDefaultToUsingTheFullyQualifiedClassNameOfTheSuppliedBeanInstance() throws Exception { + void configureBeanReallyDoesDefaultToUsingTheFullyQualifiedClassNameOfTheSuppliedBeanInstance() { TestBean beanInstance = new TestBean(); BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); builder.addPropertyValue("name", "Harriet Wheeler"); @@ -80,7 +80,7 @@ public void configureBeanReallyDoesDefaultToUsingTheFullyQualifiedClassNameOfThe } @Test - public void configureBeanPerformsAutowiringByNameIfAppropriateBeanWiringInfoResolverIsPluggedIn() throws Exception { + void configureBeanPerformsAutowiringByNameIfAppropriateBeanWiringInfoResolverIsPluggedIn() { TestBean beanInstance = new TestBean(); // spouse for autowiring by name... BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); @@ -100,7 +100,7 @@ public void configureBeanPerformsAutowiringByNameIfAppropriateBeanWiringInfoReso } @Test - public void configureBeanPerformsAutowiringByTypeIfAppropriateBeanWiringInfoResolverIsPluggedIn() throws Exception { + void configureBeanPerformsAutowiringByTypeIfAppropriateBeanWiringInfoResolverIsPluggedIn() { TestBean beanInstance = new TestBean(); // spouse for autowiring by type... BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanWiringInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanWiringInfoTests.java index 7919ba0a0b45..9cba6236a0b5 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanWiringInfoTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanWiringInfoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,63 +22,63 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the BeanWiringInfo class. + * Tests for {@link BeanWiringInfo}. * * @author Rick Evans * @author Sam Brannen */ -public class BeanWiringInfoTests { +class BeanWiringInfoTests { @Test - public void ctorWithNullBeanName() throws Exception { + void ctorWithNullBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> new BeanWiringInfo(null)); } @Test - public void ctorWithWhitespacedBeanName() throws Exception { + void ctorWithWhitespacedBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> new BeanWiringInfo(" \t")); } @Test - public void ctorWithEmptyBeanName() throws Exception { + void ctorWithEmptyBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> new BeanWiringInfo("")); } @Test - public void ctorWithNegativeIllegalAutowiringValue() throws Exception { + void ctorWithNegativeIllegalAutowiringValue() { assertThatIllegalArgumentException().isThrownBy(() -> new BeanWiringInfo(-1, true)); } @Test - public void ctorWithPositiveOutOfRangeAutowiringValue() throws Exception { + void ctorWithPositiveOutOfRangeAutowiringValue() { assertThatIllegalArgumentException().isThrownBy(() -> new BeanWiringInfo(123871, true)); } @Test - public void usingAutowireCtorIndicatesAutowiring() throws Exception { + void usingAutowireCtorIndicatesAutowiring() { BeanWiringInfo info = new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_NAME, true); assertThat(info.indicatesAutowiring()).isTrue(); } @Test - public void usingBeanNameCtorDoesNotIndicateAutowiring() throws Exception { + void usingBeanNameCtorDoesNotIndicateAutowiring() { BeanWiringInfo info = new BeanWiringInfo("fooService"); assertThat(info.indicatesAutowiring()).isFalse(); } @Test - public void noDependencyCheckValueIsPreserved() throws Exception { + void noDependencyCheckValueIsPreserved() { BeanWiringInfo info = new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_NAME, true); assertThat(info.getDependencyCheck()).isTrue(); } @Test - public void dependencyCheckValueIsPreserved() throws Exception { + void dependencyCheckValueIsPreserved() { BeanWiringInfo info = new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_TYPE, false); assertThat(info.getDependencyCheck()).isFalse(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java index becb7961f2a3..2aaa35afc24e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,14 +22,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the ClassNameBeanWiringInfoResolver class. + * Tests for {@link ClassNameBeanWiringInfoResolver}. * * @author Rick Evans */ class ClassNameBeanWiringInfoResolverTests { @Test - void resolveWiringInfoWithNullBeanInstance() throws Exception { + void resolveWiringInfoWithNullBeanInstance() { assertThatIllegalArgumentException().isThrownBy(() -> new ClassNameBeanWiringInfoResolver().resolveWiringInfo(null)); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/AutowireWithExclusionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/AutowireWithExclusionTests.java index 8ea0719a82be..8ce547df688e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/AutowireWithExclusionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/AutowireWithExclusionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,10 +31,10 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class AutowireWithExclusionTests { +class AutowireWithExclusionTests { @Test - public void byTypeAutowireWithAutoSelfExclusion() throws Exception { + void byTypeAutowireWithAutoSelfExclusion() { CountingFactory.reset(); DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-exclusion.xml"); beanFactory.preInstantiateSingletons(); @@ -45,7 +45,7 @@ public void byTypeAutowireWithAutoSelfExclusion() throws Exception { } @Test - public void byTypeAutowireWithExclusion() throws Exception { + void byTypeAutowireWithExclusion() { CountingFactory.reset(); DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-exclusion.xml"); beanFactory.preInstantiateSingletons(); @@ -55,7 +55,7 @@ public void byTypeAutowireWithExclusion() throws Exception { } @Test - public void byTypeAutowireWithExclusionInParentFactory() throws Exception { + void byTypeAutowireWithExclusionInParentFactory() { CountingFactory.reset(); DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); parent.preInstantiateSingletons(); @@ -70,7 +70,7 @@ public void byTypeAutowireWithExclusionInParentFactory() throws Exception { } @Test - public void byTypeAutowireWithPrimaryInParentFactory() throws Exception { + void byTypeAutowireWithPrimaryInParentFactory() { CountingFactory.reset(); DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); parent.getBeanDefinition("props1").setPrimary(true); @@ -89,7 +89,7 @@ public void byTypeAutowireWithPrimaryInParentFactory() throws Exception { } @Test - public void byTypeAutowireWithPrimaryOverridingParentFactory() throws Exception { + void byTypeAutowireWithPrimaryOverridingParentFactory() { CountingFactory.reset(); DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); parent.preInstantiateSingletons(); @@ -108,7 +108,7 @@ public void byTypeAutowireWithPrimaryOverridingParentFactory() throws Exception } @Test - public void byTypeAutowireWithPrimaryInParentAndChild() throws Exception { + void byTypeAutowireWithPrimaryInParentAndChild() { CountingFactory.reset(); DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); parent.getBeanDefinition("props1").setPrimary(true); @@ -128,7 +128,7 @@ public void byTypeAutowireWithPrimaryInParentAndChild() throws Exception { } @Test - public void byTypeAutowireWithInclusion() throws Exception { + void byTypeAutowireWithInclusion() { CountingFactory.reset(); DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-inclusion.xml"); beanFactory.preInstantiateSingletons(); @@ -138,7 +138,7 @@ public void byTypeAutowireWithInclusion() throws Exception { } @Test - public void byTypeAutowireWithSelectiveInclusion() throws Exception { + void byTypeAutowireWithSelectiveInclusion() { CountingFactory.reset(); DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-selective-inclusion.xml"); beanFactory.preInstantiateSingletons(); @@ -148,7 +148,7 @@ public void byTypeAutowireWithSelectiveInclusion() throws Exception { } @Test - public void constructorAutowireWithAutoSelfExclusion() throws Exception { + void constructorAutowireWithAutoSelfExclusion() { DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-constructor-with-exclusion.xml"); TestBean rob = (TestBean) beanFactory.getBean("rob"); TestBean sally = (TestBean) beanFactory.getBean("sally"); @@ -161,7 +161,7 @@ public void constructorAutowireWithAutoSelfExclusion() throws Exception { } @Test - public void constructorAutowireWithExclusion() throws Exception { + void constructorAutowireWithExclusion() { DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-constructor-with-exclusion.xml"); TestBean rob = (TestBean) beanFactory.getBean("rob"); assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props1"); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/BeanNameGenerationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/BeanNameGenerationTests.java index 11d0324d76f6..52b5d12485e6 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/BeanNameGenerationTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/BeanNameGenerationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,13 +29,13 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class BeanNameGenerationTests { +class BeanNameGenerationTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setUp() { + void setUp() { this.beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); @@ -43,7 +43,7 @@ public void setUp() { } @Test - public void naming() { + void naming() { String className = GeneratedNameBean.class.getName(); String targetName = className + BeanDefinitionReaderUtils.GENERATED_BEAN_NAME_SEPARATOR + "0"; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionMergingTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionMergingTests.java index ac739a1b76dc..809b5f853936 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionMergingTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionMergingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Properties; import java.util.Set; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,6 +31,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.io.ClassPathResource; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; /** @@ -39,47 +41,44 @@ * @author Rick Evans */ @SuppressWarnings("rawtypes") -public class CollectionMergingTests { +class CollectionMergingTests { private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @BeforeEach - public void setUp() throws Exception { + void setUp() { BeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); reader.loadBeanDefinitions(new ClassPathResource("collectionMerging.xml", getClass())); } @Test - public void mergeList() throws Exception { + void mergeList() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithList"); List list = bean.getSomeList(); - assertThat(list).as("Incorrect size").hasSize(3); - assertThat(list.get(0)).isEqualTo("Rob Harrop"); - assertThat(list.get(1)).isEqualTo("Rod Johnson"); - assertThat(list.get(2)).isEqualTo("Juergen Hoeller"); + assertThat(list).asInstanceOf(InstanceOfAssertFactories.list(String.class)) + .containsExactly("Rob Harrop", "Rod Johnson", "Juergen Hoeller"); } @Test - public void mergeListWithInnerBeanAsListElement() throws Exception { + void mergeListWithInnerBeanAsListElement() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithListOfRefs"); List list = bean.getSomeList(); assertThat(list).isNotNull(); assertThat(list).hasSize(3); - assertThat(list.get(2) instanceof TestBean).isTrue(); + assertThat(list.get(2)).isInstanceOf(TestBean.class); } @Test - public void mergeSet() { + void mergeSet() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithSet"); Set set = bean.getSomeSet(); - assertThat(set).as("Incorrect size").hasSize(2); - assertThat(set.contains("Rob Harrop")).isTrue(); - assertThat(set.contains("Sally Greenwood")).isTrue(); + assertThat(set).asInstanceOf(InstanceOfAssertFactories.collection(String.class)) + .containsOnly("Rob Harrop", "Sally Greenwood"); } @Test - public void mergeSetWithInnerBeanAsSetElement() throws Exception { + void mergeSetWithInnerBeanAsSetElement() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithSetOfRefs"); Set set = bean.getSomeSet(); assertThat(set).isNotNull(); @@ -87,33 +86,31 @@ public void mergeSetWithInnerBeanAsSetElement() throws Exception { Iterator it = set.iterator(); it.next(); Object o = it.next(); - assertThat(o instanceof TestBean).isTrue(); + assertThat(o).isInstanceOf(TestBean.class); assertThat(((TestBean) o).getName()).isEqualTo("Sally"); } @Test - public void mergeMap() throws Exception { + void mergeMap() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithMap"); Map map = bean.getSomeMap(); - assertThat(map).as("Incorrect size").hasSize(3); - assertThat(map.get("Rob")).isEqualTo("Sally"); - assertThat(map.get("Rod")).isEqualTo("Kerry"); - assertThat(map.get("Juergen")).isEqualTo("Eva"); + assertThat(map).asInstanceOf(InstanceOfAssertFactories.map(String.class,String.class)) + .containsOnly(entry("Rob", "Sally"), entry("Rod", "Kerry"), entry("Juergen", "Eva")); } @Test - public void mergeMapWithInnerBeanAsMapEntryValue() throws Exception { + void mergeMapWithInnerBeanAsMapEntryValue() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithMapOfRefs"); Map map = bean.getSomeMap(); assertThat(map).isNotNull(); assertThat(map).hasSize(2); assertThat(map.get("Rob")).isNotNull(); - assertThat(map.get("Rob") instanceof TestBean).isTrue(); + assertThat(map.get("Rob")).isInstanceOf(TestBean.class); assertThat(((TestBean) map.get("Rob")).getName()).isEqualTo("Sally"); } @Test - public void mergeProperties() throws Exception { + void mergeProperties() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithProps"); Properties props = bean.getSomeProperties(); assertThat(props).as("Incorrect size").hasSize(3); @@ -123,27 +120,22 @@ public void mergeProperties() throws Exception { } @Test - public void mergeListInConstructor() throws Exception { + void mergeListInConstructor() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithListInConstructor"); List list = bean.getSomeList(); - assertThat(list).as("Incorrect size").hasSize(3); - assertThat(list.get(0)).isEqualTo("Rob Harrop"); - assertThat(list.get(1)).isEqualTo("Rod Johnson"); - assertThat(list.get(2)).isEqualTo("Juergen Hoeller"); + assertThat(list).asInstanceOf(InstanceOfAssertFactories.list(String.class)) + .containsExactly("Rob Harrop", "Rod Johnson", "Juergen Hoeller"); } @Test - public void mergeListWithInnerBeanAsListElementInConstructor() throws Exception { + void mergeListWithInnerBeanAsListElementInConstructor() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithListOfRefsInConstructor"); List list = bean.getSomeList(); - assertThat(list).isNotNull(); - assertThat(list).hasSize(3); - assertThat(list.get(2)).isNotNull(); - assertThat(list.get(2) instanceof TestBean).isTrue(); + assertThat(list).hasSize(3).element(2).isInstanceOf(TestBean.class); } @Test - public void mergeSetInConstructor() { + void mergeSetInConstructor() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithSetInConstructor"); Set set = bean.getSomeSet(); assertThat(set).as("Incorrect size").hasSize(2); @@ -152,7 +144,7 @@ public void mergeSetInConstructor() { } @Test - public void mergeSetWithInnerBeanAsSetElementInConstructor() throws Exception { + void mergeSetWithInnerBeanAsSetElementInConstructor() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithSetOfRefsInConstructor"); Set set = bean.getSomeSet(); assertThat(set).isNotNull(); @@ -160,12 +152,12 @@ public void mergeSetWithInnerBeanAsSetElementInConstructor() throws Exception { Iterator it = set.iterator(); it.next(); Object o = it.next(); - assertThat(o instanceof TestBean).isTrue(); + assertThat(o).isInstanceOf(TestBean.class); assertThat(((TestBean) o).getName()).isEqualTo("Sally"); } @Test - public void mergeMapInConstructor() throws Exception { + void mergeMapInConstructor() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithMapInConstructor"); Map map = bean.getSomeMap(); assertThat(map).as("Incorrect size").hasSize(3); @@ -175,17 +167,17 @@ public void mergeMapInConstructor() throws Exception { } @Test - public void mergeMapWithInnerBeanAsMapEntryValueInConstructor() throws Exception { + void mergeMapWithInnerBeanAsMapEntryValueInConstructor() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithMapOfRefsInConstructor"); Map map = bean.getSomeMap(); assertThat(map).isNotNull(); assertThat(map).hasSize(2); - assertThat(map.get("Rob") instanceof TestBean).isTrue(); + assertThat(map.get("Rob")).isInstanceOf(TestBean.class); assertThat(((TestBean) map.get("Rob")).getName()).isEqualTo("Sally"); } @Test - public void mergePropertiesInConstructor() throws Exception { + void mergePropertiesInConstructor() { TestBean bean = (TestBean) this.beanFactory.getBean("childWithPropsInConstructor"); Properties props = bean.getSomeProperties(); assertThat(props).as("Incorrect size").hasSize(3); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionsWithDefaultTypesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionsWithDefaultTypesTests.java index db1c1eaab9b7..e9ed87d1181c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionsWithDefaultTypesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionsWithDefaultTypesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class CollectionsWithDefaultTypesTests { +class CollectionsWithDefaultTypesTests { private final DefaultListableBeanFactory beanFactory; @@ -42,7 +42,7 @@ public CollectionsWithDefaultTypesTests() { } @Test - public void testListHasDefaultType() throws Exception { + void testListHasDefaultType() { TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); for (Object o : bean.getSomeList()) { assertThat(o.getClass()).as("Value type is incorrect").isEqualTo(Integer.class); @@ -50,7 +50,7 @@ public void testListHasDefaultType() throws Exception { } @Test - public void testSetHasDefaultType() throws Exception { + void testSetHasDefaultType() { TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); for (Object o : bean.getSomeSet()) { assertThat(o.getClass()).as("Value type is incorrect").isEqualTo(Integer.class); @@ -58,13 +58,13 @@ public void testSetHasDefaultType() throws Exception { } @Test - public void testMapHasDefaultKeyAndValueType() throws Exception { + void testMapHasDefaultKeyAndValueType() { TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); assertMap(bean.getSomeMap()); } @Test - public void testMapWithNestedElementsHasDefaultKeyAndValueType() throws Exception { + void testMapWithNestedElementsHasDefaultKeyAndValueType() { TestBean bean = (TestBean) this.beanFactory.getBean("testBean2"); assertMap(bean.getSomeMap()); } @@ -79,14 +79,14 @@ private void assertMap(Map map) { @Test @SuppressWarnings("rawtypes") - public void testBuildCollectionFromMixtureOfReferencesAndValues() throws Exception { + public void testBuildCollectionFromMixtureOfReferencesAndValues() { MixedCollectionBean jumble = (MixedCollectionBean) this.beanFactory.getBean("jumble"); assertThat(jumble.getJumble()).as("Expected 3 elements, not " + jumble.getJumble().size()).hasSize(3); List l = (List) jumble.getJumble(); assertThat(l.get(0).equals("literal")).isTrue(); Integer[] array1 = (Integer[]) l.get(1); - assertThat(array1[0].equals(2)).isTrue(); - assertThat(array1[1].equals(4)).isTrue(); + assertThat(array1[0]).isEqualTo(2); + assertThat(array1[1]).isEqualTo(4); int[] array2 = (int[]) l.get(2); assertThat(array2[0]).isEqualTo(3); assertThat(array2[1]).isEqualTo(5); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DefaultLifecycleMethodsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DefaultLifecycleMethodsTests.java index 0c6f7f80a20f..7d69e989c3e9 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DefaultLifecycleMethodsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DefaultLifecycleMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,20 +28,20 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class DefaultLifecycleMethodsTests { +class DefaultLifecycleMethodsTests { private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @BeforeEach - public void setup() throws Exception { + void setup() { new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( new ClassPathResource("defaultLifecycleMethods.xml", getClass())); } @Test - public void lifecycleMethodsInvoked() { + void lifecycleMethodsInvoked() { LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("lifecycleAware"); assertThat(bean.isInitCalled()).as("Bean not initialized").isTrue(); assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); @@ -52,7 +52,7 @@ public void lifecycleMethodsInvoked() { } @Test - public void lifecycleMethodsDisabled() throws Exception { + void lifecycleMethodsDisabled() { LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("lifecycleMethodsDisabled"); assertThat(bean.isInitCalled()).as("Bean init method called incorrectly").isFalse(); assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); @@ -62,7 +62,7 @@ public void lifecycleMethodsDisabled() throws Exception { } @Test - public void ignoreDefaultLifecycleMethods() throws Exception { + void ignoreDefaultLifecycleMethods() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource( "ignoreDefaultLifecycleMethods.xml", getClass())); @@ -71,7 +71,7 @@ public void ignoreDefaultLifecycleMethods() throws Exception { } @Test - public void overrideDefaultLifecycleMethods() throws Exception { + void overrideDefaultLifecycleMethods() { LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("overrideLifecycleMethods"); assertThat(bean.isInitCalled()).as("Default init method called incorrectly").isFalse(); assertThat(bean.isCustomInitCalled()).as("Custom init method not called").isTrue(); @@ -81,7 +81,7 @@ public void overrideDefaultLifecycleMethods() throws Exception { } @Test - public void childWithDefaultLifecycleMethods() throws Exception { + void childWithDefaultLifecycleMethods() { LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("childWithDefaultLifecycleMethods"); assertThat(bean.isInitCalled()).as("Bean not initialized").isTrue(); assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); @@ -92,7 +92,7 @@ public void childWithDefaultLifecycleMethods() throws Exception { } @Test - public void childWithLifecycleMethodsDisabled() throws Exception { + void childWithLifecycleMethodsDisabled() { LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("childWithLifecycleMethodsDisabled"); assertThat(bean.isInitCalled()).as("Bean init method called incorrectly").isFalse(); assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DelegatingEntityResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DelegatingEntityResolverTests.java index 6e05b66c7c6b..b82c2f347d3e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DelegatingEntityResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DelegatingEntityResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,27 +23,27 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link DelegatingEntityResolver} class. + * Tests for {@link DelegatingEntityResolver}. * * @author Rick Evans * @author Chris Beams */ -public class DelegatingEntityResolverTests { +class DelegatingEntityResolverTests { @Test - public void testCtorWhereDtdEntityResolverIsNull() throws Exception { + void testCtorWhereDtdEntityResolverIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingEntityResolver(null, new NoOpEntityResolver())); } @Test - public void testCtorWhereSchemaEntityResolverIsNull() throws Exception { + void testCtorWhereSchemaEntityResolverIsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingEntityResolver(new NoOpEntityResolver(), null)); } @Test - public void testCtorWhereEntityResolversAreBothNull() throws Exception { + void testCtorWhereEntityResolversAreBothNull() { assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingEntityResolver(null, null)); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/EventPublicationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/EventPublicationTests.java index 693e2a90540c..cb775b609bc4 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/EventPublicationTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/EventPublicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,6 @@ * @author Rob Harrop * @author Juergen Hoeller */ -@SuppressWarnings("rawtypes") class EventPublicationTests { private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -50,7 +49,7 @@ class EventPublicationTests { @BeforeEach - void setUp() throws Exception { + void setUp() { XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); reader.setEventListener(this.eventListener); reader.setSourceExtractor(new PassThroughSourceExtractor()); @@ -58,10 +57,9 @@ void setUp() throws Exception { } @Test - void defaultsEventReceived() throws Exception { + void defaultsEventReceived() { List defaultsList = this.eventListener.getDefaults(); - assertThat(defaultsList).isNotEmpty(); - assertThat(defaultsList.get(0)).isInstanceOf(DocumentDefaultsDefinition.class); + assertThat(defaultsList).element(0).isInstanceOf(DocumentDefaultsDefinition.class); DocumentDefaultsDefinition defaults = (DocumentDefaultsDefinition) defaultsList.get(0); assertThat(defaults.getLazyInit()).isEqualTo("true"); assertThat(defaults.getAutowire()).isEqualTo("constructor"); @@ -72,7 +70,7 @@ void defaultsEventReceived() throws Exception { } @Test - void beanEventReceived() throws Exception { + void beanEventReceived() { ComponentDefinition componentDefinition1 = this.eventListener.getComponentDefinition("testBean"); assertThat(componentDefinition1).isInstanceOf(BeanComponentDefinition.class); assertThat(componentDefinition1.getBeanDefinitions()).hasSize(1); @@ -98,7 +96,7 @@ void beanEventReceived() throws Exception { } @Test - void aliasEventReceived() throws Exception { + void aliasEventReceived() { List aliases = this.eventListener.getAliases("testBean"); assertThat(aliases).hasSize(2); AliasDefinition aliasDefinition1 = aliases.get(0); @@ -110,7 +108,7 @@ void aliasEventReceived() throws Exception { } @Test - void importEventReceived() throws Exception { + void importEventReceived() { List imports = this.eventListener.getImports(); assertThat(imports).hasSize(1); ImportDefinition importDefinition = imports.get(0); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java index 74b599bc51d1..418b64cc815c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,10 +36,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class FactoryMethodTests { +class FactoryMethodTests { @Test - public void testFactoryMethodsSingletonOnTargetClass() { + void testFactoryMethodsSingletonOnTargetClass() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -73,7 +73,7 @@ public void testFactoryMethodsSingletonOnTargetClass() { } @Test - public void testFactoryMethodsWithInvalidDestroyMethod() { + void testFactoryMethodsWithInvalidDestroyMethod() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -82,7 +82,7 @@ public void testFactoryMethodsWithInvalidDestroyMethod() { } @Test - public void testFactoryMethodsWithNullInstance() { + void testFactoryMethodsWithNullInstance() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -93,7 +93,7 @@ public void testFactoryMethodsWithNullInstance() { } @Test - public void testFactoryMethodsWithNullValue() { + void testFactoryMethodsWithNullValue() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -115,7 +115,7 @@ public void testFactoryMethodsWithNullValue() { } @Test - public void testFactoryMethodsWithAutowire() { + void testFactoryMethodsWithAutowire() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -127,7 +127,7 @@ public void testFactoryMethodsWithAutowire() { } @Test - public void testProtectedFactoryMethod() { + void testProtectedFactoryMethod() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -137,7 +137,7 @@ public void testProtectedFactoryMethod() { } @Test - public void testPrivateFactoryMethod() { + void testPrivateFactoryMethod() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -147,7 +147,7 @@ public void testPrivateFactoryMethod() { } @Test - public void testFactoryMethodsPrototypeOnTargetClass() { + void testFactoryMethodsPrototypeOnTargetClass() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -191,7 +191,7 @@ public void testFactoryMethodsPrototypeOnTargetClass() { * Tests where the static factory method is on a different class. */ @Test - public void testFactoryMethodsOnExternalClass() { + void testFactoryMethodsOnExternalClass() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -199,8 +199,8 @@ public void testFactoryMethodsOnExternalClass() { assertThat(xbf.getType("externalFactoryMethodWithoutArgs")).isEqualTo(TestBean.class); assertThat(xbf.getType("externalFactoryMethodWithArgs")).isEqualTo(TestBean.class); String[] names = xbf.getBeanNamesForType(TestBean.class); - assertThat(Arrays.asList(names).contains("externalFactoryMethodWithoutArgs")).isTrue(); - assertThat(Arrays.asList(names).contains("externalFactoryMethodWithArgs")).isTrue(); + assertThat(Arrays.asList(names)).contains("externalFactoryMethodWithoutArgs"); + assertThat(Arrays.asList(names)).contains("externalFactoryMethodWithArgs"); TestBean tb = (TestBean) xbf.getBean("externalFactoryMethodWithoutArgs"); assertThat(tb.getAge()).isEqualTo(2); @@ -212,12 +212,12 @@ public void testFactoryMethodsOnExternalClass() { assertThat(xbf.getType("externalFactoryMethodWithoutArgs")).isEqualTo(TestBean.class); assertThat(xbf.getType("externalFactoryMethodWithArgs")).isEqualTo(TestBean.class); names = xbf.getBeanNamesForType(TestBean.class); - assertThat(Arrays.asList(names).contains("externalFactoryMethodWithoutArgs")).isTrue(); - assertThat(Arrays.asList(names).contains("externalFactoryMethodWithArgs")).isTrue(); + assertThat(Arrays.asList(names)).contains("externalFactoryMethodWithoutArgs"); + assertThat(Arrays.asList(names)).contains("externalFactoryMethodWithArgs"); } @Test - public void testInstanceFactoryMethodWithoutArgs() { + void testInstanceFactoryMethodWithoutArgs() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -235,7 +235,7 @@ public void testInstanceFactoryMethodWithoutArgs() { } @Test - public void testFactoryMethodNoMatchingStaticMethod() { + void testFactoryMethodNoMatchingStaticMethod() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -244,7 +244,7 @@ public void testFactoryMethodNoMatchingStaticMethod() { } @Test - public void testNonExistingFactoryMethod() { + void testNonExistingFactoryMethod() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -254,7 +254,7 @@ public void testNonExistingFactoryMethod() { } @Test - public void testFactoryMethodArgumentsForNonExistingMethod() { + void testFactoryMethodArgumentsForNonExistingMethod() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -264,7 +264,7 @@ public void testFactoryMethodArgumentsForNonExistingMethod() { } @Test - public void testCanSpecifyFactoryMethodArgumentsOnFactoryMethodPrototype() { + void testCanSpecifyFactoryMethodArgumentsOnFactoryMethodPrototype() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -300,7 +300,7 @@ public void testCanSpecifyFactoryMethodArgumentsOnFactoryMethodPrototype() { } @Test - public void testCanSpecifyFactoryMethodArgumentsOnSingleton() { + void testCanSpecifyFactoryMethodArgumentsOnSingleton() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -315,7 +315,7 @@ public void testCanSpecifyFactoryMethodArgumentsOnSingleton() { } @Test - public void testCannotSpecifyFactoryMethodArgumentsOnSingletonAfterCreation() { + void testCannotSpecifyFactoryMethodArgumentsOnSingletonAfterCreation() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -329,7 +329,7 @@ public void testCannotSpecifyFactoryMethodArgumentsOnSingletonAfterCreation() { } @Test - public void testFactoryMethodWithDifferentReturnType() { + void testFactoryMethodWithDifferentReturnType() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); @@ -337,22 +337,22 @@ public void testFactoryMethodWithDifferentReturnType() { // Check that listInstance is not considered a bean of type FactoryMethods. assertThat(List.class.isAssignableFrom(xbf.getType("listInstance"))).isTrue(); String[] names = xbf.getBeanNamesForType(FactoryMethods.class); - assertThat(Arrays.asList(names).contains("listInstance")).isFalse(); + assertThat(Arrays.asList(names)).doesNotContain("listInstance"); names = xbf.getBeanNamesForType(List.class); - assertThat(Arrays.asList(names).contains("listInstance")).isTrue(); + assertThat(Arrays.asList(names)).contains("listInstance"); xbf.preInstantiateSingletons(); assertThat(List.class.isAssignableFrom(xbf.getType("listInstance"))).isTrue(); names = xbf.getBeanNamesForType(FactoryMethods.class); - assertThat(Arrays.asList(names).contains("listInstance")).isFalse(); + assertThat(Arrays.asList(names)).doesNotContain("listInstance"); names = xbf.getBeanNamesForType(List.class); - assertThat(Arrays.asList(names).contains("listInstance")).isTrue(); + assertThat(Arrays.asList(names)).contains("listInstance"); List list = (List) xbf.getBean("listInstance"); assertThat(list).isEqualTo(Collections.EMPTY_LIST); } @Test - public void testFactoryMethodForJavaMailSession() { + void testFactoryMethodForJavaMailSession() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/MetadataAttachmentTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/MetadataAttachmentTests.java index 1c796266d1f5..9548ca562728 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/MetadataAttachmentTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/MetadataAttachmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,33 +29,33 @@ /** * @author Rob Harrop */ -public class MetadataAttachmentTests { +class MetadataAttachmentTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setUp() throws Exception { + void setUp() { this.beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( new ClassPathResource("withMeta.xml", getClass())); } @Test - public void metadataAttachment() throws Exception { + void metadataAttachment() { BeanDefinition beanDefinition1 = this.beanFactory.getMergedBeanDefinition("testBean1"); assertThat(beanDefinition1.getAttribute("foo")).isEqualTo("bar"); } @Test - public void metadataIsInherited() throws Exception { + void metadataIsInherited() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("testBean2"); assertThat(beanDefinition.getAttribute("foo")).as("Metadata not inherited").isEqualTo("bar"); assertThat(beanDefinition.getAttribute("abc")).as("Child metdata not attached").isEqualTo("123"); } @Test - public void propertyMetadata() throws Exception { + void propertyMetadata() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("testBean3"); PropertyValue pv = beanDefinition.getPropertyValues().getPropertyValue("name"); assertThat(pv.getAttribute("surname")).isEqualTo("Harrop"); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java index cce3bb3ca15b..4efa9b5dd386 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ * * @author Chris Beams */ -public class NestedBeansElementAttributeRecursionTests { +class NestedBeansElementAttributeRecursionTests { @Test - public void defaultLazyInit() { + void defaultLazyInit() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", this.getClass())); @@ -42,7 +42,7 @@ public void defaultLazyInit() { } @Test - public void defaultLazyInitWithNonValidatingParser() { + void defaultLazyInitWithNonValidatingParser() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); xmlBeanDefinitionReader.setValidating(false); @@ -67,7 +67,7 @@ private void assertLazyInits(DefaultListableBeanFactory bf) { } @Test - public void defaultMerge() { + void defaultMerge() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", this.getClass())); @@ -76,7 +76,7 @@ public void defaultMerge() { } @Test - public void defaultMergeWithNonValidatingParser() { + void defaultMergeWithNonValidatingParser() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); xmlBeanDefinitionReader.setValidating(false); @@ -106,7 +106,7 @@ private void assertMerge(DefaultListableBeanFactory bf) { } @Test - public void defaultAutowireCandidates() { + void defaultAutowireCandidates() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", this.getClass())); @@ -115,7 +115,7 @@ public void defaultAutowireCandidates() { } @Test - public void defaultAutowireCandidatesWithNonValidatingParser() { + void defaultAutowireCandidatesWithNonValidatingParser() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); xmlBeanDefinitionReader.setValidating(false); @@ -146,7 +146,7 @@ private void assertAutowireCandidates(DefaultListableBeanFactory bf) { } @Test - public void initMethod() { + void initMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("NestedBeansElementAttributeRecursionTests-init-destroy-context.xml", this.getClass())); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java index 4117c82c4329..44cccf0b72fe 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,12 +32,12 @@ * * @author Chris Beams */ -public class NestedBeansElementTests { +class NestedBeansElementTests { private final Resource XML = new ClassPathResource("NestedBeansElementTests-context.xml", this.getClass()); @Test - public void getBean_withoutActiveProfile() { + void getBean_withoutActiveProfile() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(XML); @@ -46,7 +46,7 @@ public void getBean_withoutActiveProfile() { } @Test - public void getBean_withActiveProfile() { + void getBean_withActiveProfile() { ConfigurableEnvironment env = new StandardEnvironment(); env.setActiveProfiles("dev"); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests.java index 56795fe75735..cb5ad2d653b4 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ * @author Sam Brannen * @since 3.1 */ -public class ProfileXmlBeanDefinitionTests { +class ProfileXmlBeanDefinitionTests { private static final String PROD_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-prodProfile.xml"; private static final String DEV_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-devProfile.xml"; @@ -61,13 +61,13 @@ public class ProfileXmlBeanDefinitionTests { private static final String TARGET_BEAN = "foo"; @Test - public void testProfileValidation() { + void testProfileValidation() { assertThatIllegalArgumentException().isThrownBy(() -> beanFactoryFor(PROD_ELIGIBLE_XML, NULL_ACTIVE)); } @Test - public void testProfilePermutations() { + void testProfilePermutations() { assertThat(beanFactoryFor(PROD_ELIGIBLE_XML, NONE_ACTIVE)).isNot(containingTarget()); assertThat(beanFactoryFor(PROD_ELIGIBLE_XML, DEV_ACTIVE)).isNot(containingTarget()); assertThat(beanFactoryFor(PROD_ELIGIBLE_XML, PROD_ACTIVE)).is(containingTarget()); @@ -116,7 +116,7 @@ public void testProfilePermutations() { } @Test - public void testDefaultProfile() { + void testDefaultProfile() { { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); @@ -140,7 +140,7 @@ public void testDefaultProfile() { } @Test - public void testDefaultAndNonDefaultProfile() { + void testDefaultAndNonDefaultProfile() { assertThat(beanFactoryFor(DEFAULT_ELIGIBLE_XML, NONE_ACTIVE)).is(containingTarget()); assertThat(beanFactoryFor(DEFAULT_ELIGIBLE_XML, "other")).isNot(containingTarget()); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java index 1b17f7660228..5229dc46f434 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for ResourceEntityResolver. + * Tests for {@link ResourceEntityResolver}. * * @author Simon Baslé * @author Sam Brannen diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SchemaValidationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SchemaValidationTests.java index 70199450995b..3f71ff9e8bb8 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SchemaValidationTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SchemaValidationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ /** * @author Rob Harrop */ -public class SchemaValidationTests { +class SchemaValidationTests { @Test - public void withAutodetection() throws Exception { + void withAutodetection() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); assertThatExceptionOfType(BeansException.class).isThrownBy(() -> @@ -42,7 +42,7 @@ public void withAutodetection() throws Exception { } @Test - public void withExplicitValidationMode() throws Exception { + void withExplicitValidationMode() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); @@ -52,7 +52,7 @@ public void withExplicitValidationMode() throws Exception { } @Test - public void loadDefinitions() throws Exception { + void loadDefinitions() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandlerTests.java index 24a9d43e89be..4f1a5535cdac 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandlerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ /** * @author Costin Leau */ -public class SimpleConstructorNamespaceHandlerTests { +class SimpleConstructorNamespaceHandlerTests { @Test - public void simpleValue() throws Exception { + void simpleValue() { DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); String name = "simple"; // beanFactory.getBean("simple1", DummyBean.class); @@ -42,7 +42,7 @@ public void simpleValue() throws Exception { } @Test - public void simpleRef() throws Exception { + void simpleRef() { DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); String name = "simple-ref"; // beanFactory.getBean("name-value1", TestBean.class); @@ -51,7 +51,7 @@ public void simpleRef() throws Exception { } @Test - public void nameValue() throws Exception { + void nameValue() { DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); String name = "name-value"; // beanFactory.getBean("name-value1", TestBean.class); @@ -61,7 +61,7 @@ public void nameValue() throws Exception { } @Test - public void nameRef() throws Exception { + void nameRef() { DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); TestBean nameValue = beanFactory.getBean("name-value", TestBean.class); DummyBean nameRef = beanFactory.getBean("name-ref", DummyBean.class); @@ -71,7 +71,7 @@ public void nameRef() throws Exception { } @Test - public void typeIndexedValue() throws Exception { + void typeIndexedValue() { DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); DummyBean typeRef = beanFactory.getBean("indexed-value", DummyBean.class); @@ -81,7 +81,7 @@ public void typeIndexedValue() throws Exception { } @Test - public void typeIndexedRef() throws Exception { + void typeIndexedRef() { DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); DummyBean typeRef = beanFactory.getBean("indexed-ref", DummyBean.class); @@ -90,7 +90,7 @@ public void typeIndexedRef() throws Exception { } @Test - public void ambiguousConstructor() throws Exception { + void ambiguousConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> new XmlBeanDefinitionReader(bf).loadBeanDefinitions( @@ -98,7 +98,7 @@ public void ambiguousConstructor() throws Exception { } @Test - public void constructorWithNameEndingInRef() throws Exception { + void constructorWithNameEndingInRef() { DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); DummyBean derivedBean = beanFactory.getBean("beanWithRefConstructorArg", DummyBean.class); assertThat(derivedBean.getAge()).isEqualTo(10); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerTests.java index 68ef31d9a82f..77fca219c25b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * @author Juergen Hoeller * @author Arjen Poutsma */ -public class SimplePropertyNamespaceHandlerTests { +class SimplePropertyNamespaceHandlerTests { @Test - public void simpleBeanConfigured() throws Exception { + void simpleBeanConfigured() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( new ClassPathResource("simplePropertyNamespaceHandlerTests.xml", getClass())); @@ -47,7 +47,7 @@ public void simpleBeanConfigured() throws Exception { } @Test - public void innerBeanConfigured() throws Exception { + void innerBeanConfigured() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( new ClassPathResource("simplePropertyNamespaceHandlerTests.xml", getClass())); @@ -59,7 +59,7 @@ public void innerBeanConfigured() throws Exception { } @Test - public void withPropertyDefinedTwice() throws Exception { + void withPropertyDefinedTwice() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( @@ -67,7 +67,7 @@ public void withPropertyDefinedTwice() throws Exception { } @Test - public void propertyWithNameEndingInRef() throws Exception { + void propertyWithNameEndingInRef() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( new ClassPathResource("simplePropertyNamespaceHandlerTests.xml", getClass())); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java index 73329e53e6ae..f321e20a3023 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.Set; import java.util.TreeMap; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,7 +47,7 @@ * @author Mark Fisher */ @SuppressWarnings("rawtypes") -public class UtilNamespaceHandlerTests { +class UtilNamespaceHandlerTests { private DefaultListableBeanFactory beanFactory; @@ -125,36 +126,41 @@ void testScopedMap() { @Test void testSimpleList() { - List list = (List) this.beanFactory.getBean("simpleList"); - assertThat(list.get(0)).isEqualTo("Rob Harrop"); - List list2 = (List) this.beanFactory.getBean("simpleList"); - assertThat(list).isSameAs(list2); + assertThat(this.beanFactory.getBean("simpleList")) + .asInstanceOf(InstanceOfAssertFactories.LIST).element(0).isEqualTo("Rob Harrop"); + assertThat(this.beanFactory.getBean("simpleList")) + .isSameAs(this.beanFactory.getBean("simpleList")); } @Test void testScopedList() { - List list = (List) this.beanFactory.getBean("scopedList"); - assertThat(list.get(0)).isEqualTo("Rob Harrop"); - List list2 = (List) this.beanFactory.getBean("scopedList"); - assertThat(list2.get(0)).isEqualTo("Rob Harrop"); - assertThat(list).isNotSameAs(list2); + assertThat(this.beanFactory.getBean("scopedList")) + .asInstanceOf(InstanceOfAssertFactories.LIST).element(0).isEqualTo("Rob Harrop"); + assertThat(this.beanFactory.getBean("scopedList")) + .asInstanceOf(InstanceOfAssertFactories.LIST).element(0).isEqualTo("Rob Harrop"); + assertThat(this.beanFactory.getBean("scopedList")) + .isNotSameAs(this.beanFactory.getBean("scopedList")); } @Test void testSimpleSet() { - Set set = (Set) this.beanFactory.getBean("simpleSet"); - assertThat(set.contains("Rob Harrop")).isTrue(); - Set set2 = (Set) this.beanFactory.getBean("simpleSet"); - assertThat(set).isSameAs(set2); + assertThat(this.beanFactory.getBean("simpleSet")).isInstanceOf(Set.class) + .asInstanceOf(InstanceOfAssertFactories.collection(String.class)) + .containsOnly("Rob Harrop"); + assertThat(this.beanFactory.getBean("simpleSet")) + .isSameAs(this.beanFactory.getBean("simpleSet")); } @Test void testScopedSet() { - Set set = (Set) this.beanFactory.getBean("scopedSet"); - assertThat(set.contains("Rob Harrop")).isTrue(); - Set set2 = (Set) this.beanFactory.getBean("scopedSet"); - assertThat(set2.contains("Rob Harrop")).isTrue(); - assertThat(set).isNotSameAs(set2); + assertThat(this.beanFactory.getBean("scopedSet")).isInstanceOf(Set.class) + .asInstanceOf(InstanceOfAssertFactories.collection(String.class)) + .containsOnly("Rob Harrop"); + assertThat(this.beanFactory.getBean("scopedSet")).isInstanceOf(Set.class) + .asInstanceOf(InstanceOfAssertFactories.collection(String.class)) + .containsOnly("Rob Harrop"); + assertThat(this.beanFactory.getBean("scopedSet")) + .isNotSameAs(this.beanFactory.getBean("scopedSet")); } @Test @@ -175,95 +181,71 @@ void testMapWithTypes() { void testNestedCollections() { TestBean bean = (TestBean) this.beanFactory.getBean("nestedCollectionsBean"); - List list = bean.getSomeList(); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo("foo"); - - Set set = bean.getSomeSet(); - assertThat(set).hasSize(1); - assertThat(set.contains("bar")).isTrue(); - - Map map = bean.getSomeMap(); - assertThat(map).hasSize(1); - assertThat(map.get("foo")).isInstanceOf(Set.class); - Set innerSet = (Set) map.get("foo"); - assertThat(innerSet).hasSize(1); - assertThat(innerSet.contains("bar")).isTrue(); + assertThat(bean.getSomeList()).singleElement().isEqualTo("foo"); + assertThat(bean.getSomeSet()).singleElement().isEqualTo("bar"); + assertThat(bean.getSomeMap()).hasSize(1).allSatisfy((key, nested) -> { + assertThat(key).isEqualTo("foo"); + assertThat(nested).isInstanceOf(Set.class).asInstanceOf( + InstanceOfAssertFactories.collection(String.class)).containsOnly("bar"); + }); TestBean bean2 = (TestBean) this.beanFactory.getBean("nestedCollectionsBean"); - assertThat(bean2.getSomeList()).isEqualTo(list); - assertThat(bean2.getSomeSet()).isEqualTo(set); - assertThat(bean2.getSomeMap()).isEqualTo(map); - assertThat(list).isNotSameAs(bean2.getSomeList()); - assertThat(set).isNotSameAs(bean2.getSomeSet()); - assertThat(map).isNotSameAs(bean2.getSomeMap()); + assertThat(bean2.getSomeList()).isEqualTo(bean.getSomeList()); + assertThat(bean2.getSomeSet()).isEqualTo(bean.getSomeSet()); + assertThat(bean2.getSomeMap()).isEqualTo(bean.getSomeMap()); + assertThat(bean.getSomeList()).isNotSameAs(bean2.getSomeList()); + assertThat(bean.getSomeSet()).isNotSameAs(bean2.getSomeSet()); + assertThat(bean.getSomeMap()).isNotSameAs(bean2.getSomeMap()); } @Test void testNestedShortcutCollections() { TestBean bean = (TestBean) this.beanFactory.getBean("nestedShortcutCollections"); - assertThat(bean.getStringArray()).hasSize(1); - assertThat(bean.getStringArray()[0]).isEqualTo("fooStr"); - - List list = bean.getSomeList(); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo("foo"); - - Set set = bean.getSomeSet(); - assertThat(set).hasSize(1); - assertThat(set.contains("bar")).isTrue(); + assertThat(bean.getStringArray()).containsExactly("fooStr"); + assertThat(bean.getSomeList()).singleElement().isEqualTo("foo"); + assertThat(bean.getSomeSet()).singleElement().isEqualTo("bar"); TestBean bean2 = (TestBean) this.beanFactory.getBean("nestedShortcutCollections"); assertThat(Arrays.equals(bean.getStringArray(), bean2.getStringArray())).isTrue(); assertThat(bean.getStringArray()).isNotSameAs(bean2.getStringArray()); - assertThat(bean2.getSomeList()).isEqualTo(list); - assertThat(bean2.getSomeSet()).isEqualTo(set); - assertThat(list).isNotSameAs(bean2.getSomeList()); - assertThat(set).isNotSameAs(bean2.getSomeSet()); + assertThat(bean2.getSomeList()).isEqualTo(bean.getSomeList()); + assertThat(bean2.getSomeSet()).isEqualTo(bean.getSomeSet()); + assertThat(bean.getSomeList()).isNotSameAs(bean2.getSomeList()); + assertThat(bean.getSomeSet()).isNotSameAs(bean2.getSomeSet()); } @Test void testNestedInCollections() { TestBean bean = (TestBean) this.beanFactory.getBean("nestedCustomTagBean"); - List list = bean.getSomeList(); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo(Integer.MIN_VALUE); - - Set set = bean.getSomeSet(); - assertThat(set).hasSize(2); - assertThat(set.contains(Thread.State.NEW)).isTrue(); - assertThat(set.contains(Thread.State.RUNNABLE)).isTrue(); - - Map map = bean.getSomeMap(); - assertThat(map).hasSize(1); - assertThat(map.get("min")).isEqualTo(CustomEnum.VALUE_1); + assertThat(bean.getSomeList()).singleElement().isEqualTo(Integer.MIN_VALUE); + assertThat(bean.getSomeSet()).asInstanceOf(InstanceOfAssertFactories.collection(Thread.State.class)) + .containsOnly(Thread.State.NEW,Thread.State.RUNNABLE); + assertThat(bean.getSomeMap()).hasSize(1).allSatisfy((key, value) -> { + assertThat(key).isEqualTo("min"); + assertThat(value).isEqualTo(CustomEnum.VALUE_1); + }); TestBean bean2 = (TestBean) this.beanFactory.getBean("nestedCustomTagBean"); - assertThat(bean2.getSomeList()).isEqualTo(list); - assertThat(bean2.getSomeSet()).isEqualTo(set); - assertThat(bean2.getSomeMap()).isEqualTo(map); - assertThat(list).isNotSameAs(bean2.getSomeList()); - assertThat(set).isNotSameAs(bean2.getSomeSet()); - assertThat(map).isNotSameAs(bean2.getSomeMap()); + assertThat(bean2.getSomeList()).isEqualTo(bean.getSomeList()); + assertThat(bean2.getSomeSet()).isEqualTo(bean.getSomeSet()); + assertThat(bean2.getSomeMap()).isEqualTo(bean.getSomeMap()); + assertThat(bean.getSomeList()).isNotSameAs(bean2.getSomeList()); + assertThat(bean.getSomeSet()).isNotSameAs(bean2.getSomeSet()); + assertThat(bean.getSomeMap()).isNotSameAs(bean2.getSomeMap()); } @Test void testCircularCollections() { TestBean bean = (TestBean) this.beanFactory.getBean("circularCollectionsBean"); - List list = bean.getSomeList(); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo(bean); - - Set set = bean.getSomeSet(); - assertThat(set).hasSize(1); - assertThat(set.contains(bean)).isTrue(); - - Map map = bean.getSomeMap(); - assertThat(map).hasSize(1); - assertThat(map.get("foo")).isEqualTo(bean); + assertThat(bean.getSomeList()).singleElement().isEqualTo(bean); + assertThat(bean.getSomeSet()).singleElement().isEqualTo(bean); + assertThat(bean.getSomeMap()).hasSize(1).allSatisfy((key, value) -> { + assertThat(key).isEqualTo("foo"); + assertThat(value).isEqualTo(bean); + }); } @Test @@ -273,18 +255,18 @@ void testCircularCollectionBeansStartingWithList() { List list = bean.getSomeList(); assertThat(Proxy.isProxyClass(list.getClass())).isTrue(); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo(bean); + assertThat(list).singleElement().isEqualTo(bean); Set set = bean.getSomeSet(); assertThat(Proxy.isProxyClass(set.getClass())).isFalse(); - assertThat(set).hasSize(1); - assertThat(set.contains(bean)).isTrue(); + assertThat(set).singleElement().isEqualTo(bean); Map map = bean.getSomeMap(); assertThat(Proxy.isProxyClass(map.getClass())).isFalse(); - assertThat(map).hasSize(1); - assertThat(map.get("foo")).isEqualTo(bean); + assertThat(map).hasSize(1).allSatisfy((key, value) -> { + assertThat(key).isEqualTo("foo"); + assertThat(value).isEqualTo(bean); + }); } @Test @@ -294,18 +276,18 @@ void testCircularCollectionBeansStartingWithSet() { List list = bean.getSomeList(); assertThat(Proxy.isProxyClass(list.getClass())).isFalse(); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo(bean); + assertThat(list).singleElement().isEqualTo(bean); Set set = bean.getSomeSet(); assertThat(Proxy.isProxyClass(set.getClass())).isTrue(); - assertThat(set).hasSize(1); - assertThat(set.contains(bean)).isTrue(); + assertThat(set).singleElement().isEqualTo(bean); Map map = bean.getSomeMap(); assertThat(Proxy.isProxyClass(map.getClass())).isFalse(); - assertThat(map).hasSize(1); - assertThat(map.get("foo")).isEqualTo(bean); + assertThat(map).hasSize(1).allSatisfy((key, value) -> { + assertThat(key).isEqualTo("foo"); + assertThat(value).isEqualTo(bean); + }); } @Test @@ -315,18 +297,18 @@ void testCircularCollectionBeansStartingWithMap() { List list = bean.getSomeList(); assertThat(Proxy.isProxyClass(list.getClass())).isFalse(); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo(bean); + assertThat(list).singleElement().isEqualTo(bean); Set set = bean.getSomeSet(); assertThat(Proxy.isProxyClass(set.getClass())).isFalse(); - assertThat(set).hasSize(1); - assertThat(set.contains(bean)).isTrue(); + assertThat(set).singleElement().isEqualTo(bean); Map map = bean.getSomeMap(); assertThat(Proxy.isProxyClass(map.getClass())).isTrue(); - assertThat(map).hasSize(1); - assertThat(map.get("foo")).isEqualTo(bean); + assertThat(map).hasSize(1).allSatisfy((key, value) -> { + assertThat(key).isEqualTo("foo"); + assertThat(value).isEqualTo(bean); + }); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java index 9d853af3e658..37101d31f28a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.io.ClassPathResource; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -53,38 +54,38 @@ * @since 19.12.2004 */ @SuppressWarnings({ "rawtypes", "unchecked" }) -public class XmlBeanCollectionTests { +class XmlBeanCollectionTests { private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @BeforeEach - public void loadBeans() { + void loadBeans() { new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( new ClassPathResource("collections.xml", getClass())); } @Test - public void testCollectionFactoryDefaults() throws Exception { + void testCollectionFactoryDefaults() throws Exception { ListFactoryBean listFactory = new ListFactoryBean(); listFactory.setSourceList(new LinkedList()); listFactory.afterPropertiesSet(); - assertThat(listFactory.getObject() instanceof ArrayList).isTrue(); + assertThat(listFactory.getObject()).isInstanceOf(ArrayList.class); SetFactoryBean setFactory = new SetFactoryBean(); setFactory.setSourceSet(new TreeSet()); setFactory.afterPropertiesSet(); - assertThat(setFactory.getObject() instanceof LinkedHashSet).isTrue(); + assertThat(setFactory.getObject()).isInstanceOf(LinkedHashSet.class); MapFactoryBean mapFactory = new MapFactoryBean(); mapFactory.setSourceMap(new TreeMap()); mapFactory.afterPropertiesSet(); - assertThat(mapFactory.getObject() instanceof LinkedHashMap).isTrue(); + assertThat(mapFactory.getObject()).isInstanceOf(LinkedHashMap.class); } @Test - public void testRefSubelement() { + void testRefSubelement() { //assertTrue("5 beans in reftypes, not " + this.beanFactory.getBeanDefinitionCount(), this.beanFactory.getBeanDefinitionCount() == 5); TestBean jen = (TestBean) this.beanFactory.getBean("jenny"); TestBean dave = (TestBean) this.beanFactory.getBean("david"); @@ -92,25 +93,25 @@ public void testRefSubelement() { } @Test - public void testPropertyWithLiteralValueSubelement() { + void testPropertyWithLiteralValueSubelement() { TestBean verbose = (TestBean) this.beanFactory.getBean("verbose"); assertThat(verbose.getName()).isEqualTo("verbose"); } @Test - public void testPropertyWithIdRefLocalAttrSubelement() { + void testPropertyWithIdRefLocalAttrSubelement() { TestBean verbose = (TestBean) this.beanFactory.getBean("verbose2"); assertThat(verbose.getName()).isEqualTo("verbose"); } @Test - public void testPropertyWithIdRefBeanAttrSubelement() { + void testPropertyWithIdRefBeanAttrSubelement() { TestBean verbose = (TestBean) this.beanFactory.getBean("verbose3"); assertThat(verbose.getName()).isEqualTo("verbose"); } @Test - public void testRefSubelementsBuildCollection() { + void testRefSubelementsBuildCollection() { TestBean jen = (TestBean) this.beanFactory.getBean("jenny"); TestBean dave = (TestBean) this.beanFactory.getBean("david"); TestBean rod = (TestBean) this.beanFactory.getBean("rod"); @@ -127,7 +128,7 @@ public void testRefSubelementsBuildCollection() { } @Test - public void testRefSubelementsBuildCollectionWithPrototypes() { + void testRefSubelementsBuildCollectionWithPrototypes() { TestBean jen = (TestBean) this.beanFactory.getBean("pJenny"); TestBean dave = (TestBean) this.beanFactory.getBean("pDavid"); TestBean rod = (TestBean) this.beanFactory.getBean("pRod"); @@ -150,17 +151,16 @@ public void testRefSubelementsBuildCollectionWithPrototypes() { } @Test - public void testRefSubelementsBuildCollectionFromSingleElement() { + void testRefSubelementsBuildCollectionFromSingleElement() { TestBean loner = (TestBean) this.beanFactory.getBean("loner"); TestBean dave = (TestBean) this.beanFactory.getBean("david"); - assertThat(loner.getFriends().size()).isEqualTo(1); - assertThat(loner.getFriends().contains(dave)).isTrue(); + assertThat(loner.getFriends()).containsOnly(dave); } @Test - public void testBuildCollectionFromMixtureOfReferencesAndValues() { + void testBuildCollectionFromMixtureOfReferencesAndValues() { MixedCollectionBean jumble = (MixedCollectionBean) this.beanFactory.getBean("jumble"); - assertThat(jumble.getJumble().size()).as("Expected 5 elements, not " + jumble.getJumble().size()).isEqualTo(5); + assertThat(jumble.getJumble()).as("Expected 5 elements, not " + jumble.getJumble()).hasSize(5); List l = (List) jumble.getJumble(); assertThat(l.get(0).equals(this.beanFactory.getBean("david"))).isTrue(); assertThat(l.get(1).equals("literal")).isTrue(); @@ -172,7 +172,7 @@ public void testBuildCollectionFromMixtureOfReferencesAndValues() { } @Test - public void testInvalidBeanNameReference() { + void testInvalidBeanNameReference() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> this.beanFactory.getBean("jumble2")) .withCauseInstanceOf(BeanDefinitionStoreException.class) @@ -180,97 +180,97 @@ public void testInvalidBeanNameReference() { } @Test - public void testEmptyMap() { + void testEmptyMap() { HasMap hasMap = (HasMap) this.beanFactory.getBean("emptyMap"); - assertThat(hasMap.getMap().size()).isEqualTo(0); + assertThat(hasMap.getMap()).hasSize(0); } @Test - public void testMapWithLiteralsOnly() { + void testMapWithLiteralsOnly() { HasMap hasMap = (HasMap) this.beanFactory.getBean("literalMap"); - assertThat(hasMap.getMap().size()).isEqualTo(3); + assertThat(hasMap.getMap()).hasSize(3); assertThat(hasMap.getMap().get("foo").equals("bar")).isTrue(); assertThat(hasMap.getMap().get("fi").equals("fum")).isTrue(); assertThat(hasMap.getMap().get("fa")).isNull(); } @Test - public void testMapWithLiteralsAndReferences() { + void testMapWithLiteralsAndReferences() { HasMap hasMap = (HasMap) this.beanFactory.getBean("mixedMap"); - assertThat(hasMap.getMap().size()).isEqualTo(5); - assertThat(hasMap.getMap().get("foo").equals(10)).isTrue(); + assertThat(hasMap.getMap()).hasSize(5); + assertThat(hasMap.getMap().get("foo")).isEqualTo(10); TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); assertThat(hasMap.getMap().get("jenny")).isSameAs(jenny); assertThat(hasMap.getMap().get(5).equals("david")).isTrue(); - assertThat(hasMap.getMap().get("bar") instanceof Long).isTrue(); - assertThat(hasMap.getMap().get("bar").equals(100L)).isTrue(); - assertThat(hasMap.getMap().get("baz") instanceof Integer).isTrue(); - assertThat(hasMap.getMap().get("baz").equals(200)).isTrue(); + assertThat(hasMap.getMap().get("bar")).isInstanceOf(Long.class); + assertThat(hasMap.getMap().get("bar")).isEqualTo(100L); + assertThat(hasMap.getMap().get("baz")).isInstanceOf(Integer.class); + assertThat(hasMap.getMap().get("baz")).isEqualTo(200); } @Test - public void testMapWithLiteralsAndPrototypeReferences() { + void testMapWithLiteralsAndPrototypeReferences() { TestBean jenny = (TestBean) this.beanFactory.getBean("pJenny"); HasMap hasMap = (HasMap) this.beanFactory.getBean("pMixedMap"); - assertThat(hasMap.getMap().size()).isEqualTo(2); + assertThat(hasMap.getMap()).hasSize(2); assertThat(hasMap.getMap().get("foo").equals("bar")).isTrue(); assertThat(hasMap.getMap().get("jenny").toString()).isEqualTo(jenny.toString()); assertThat(hasMap.getMap().get("jenny")).as("Not same instance").isNotSameAs(jenny); HasMap hasMap2 = (HasMap) this.beanFactory.getBean("pMixedMap"); - assertThat(hasMap2.getMap().size()).isEqualTo(2); + assertThat(hasMap2.getMap()).hasSize(2); assertThat(hasMap2.getMap().get("foo").equals("bar")).isTrue(); assertThat(hasMap2.getMap().get("jenny").toString()).isEqualTo(jenny.toString()); assertThat(hasMap2.getMap().get("jenny")).as("Not same instance").isNotSameAs(hasMap.getMap().get("jenny")); } @Test - public void testMapWithLiteralsReferencesAndList() { + void testMapWithLiteralsReferencesAndList() { HasMap hasMap = (HasMap) this.beanFactory.getBean("mixedMapWithList"); - assertThat(hasMap.getMap().size()).isEqualTo(4); + assertThat(hasMap.getMap()).hasSize(4); assertThat(hasMap.getMap().get(null).equals("bar")).isTrue(); TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); - assertThat(hasMap.getMap().get("jenny").equals(jenny)).isTrue(); + assertThat(hasMap.getMap().get("jenny")).isEqualTo(jenny); // Check list List l = (List) hasMap.getMap().get("list"); assertThat(l).isNotNull(); - assertThat(l.size()).isEqualTo(4); + assertThat(l).hasSize(4); assertThat(l.get(0).equals("zero")).isTrue(); - assertThat(l.get(3)).isNull(); + assertThat(l).element(3).isNull(); // Check nested map in list Map m = (Map) l.get(1); assertThat(m).isNotNull(); - assertThat(m.size()).isEqualTo(2); + assertThat(m).hasSize(2); assertThat(m.get("fo").equals("bar")).isTrue(); assertThat(m.get("jen").equals(jenny)).as("Map element 'jenny' should be equal to jenny bean, not " + m.get("jen")).isTrue(); // Check nested list in list l = (List) l.get(2); assertThat(l).isNotNull(); - assertThat(l.size()).isEqualTo(2); - assertThat(l.get(0).equals(jenny)).isTrue(); + assertThat(l).hasSize(2); + assertThat(l.get(0)).isEqualTo(jenny); assertThat(l.get(1).equals("ba")).isTrue(); // Check nested map m = (Map) hasMap.getMap().get("map"); assertThat(m).isNotNull(); - assertThat(m.size()).isEqualTo(2); + assertThat(m).hasSize(2); assertThat(m.get("foo").equals("bar")).isTrue(); assertThat(m.get("jenny").equals(jenny)).as("Map element 'jenny' should be equal to jenny bean, not " + m.get("jenny")).isTrue(); } @Test - public void testEmptySet() { + void testEmptySet() { HasMap hasMap = (HasMap) this.beanFactory.getBean("emptySet"); - assertThat(hasMap.getSet().size()).isEqualTo(0); + assertThat(hasMap.getSet()).hasSize(0); } @Test - public void testPopulatedSet() { + void testPopulatedSet() { HasMap hasMap = (HasMap) this.beanFactory.getBean("set"); - assertThat(hasMap.getSet().size()).isEqualTo(3); + assertThat(hasMap.getSet()).hasSize(3); assertThat(hasMap.getSet().contains("bar")).isTrue(); TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); assertThat(hasMap.getSet().contains(jenny)).isTrue(); @@ -282,9 +282,9 @@ public void testPopulatedSet() { } @Test - public void testPopulatedConcurrentSet() { + void testPopulatedConcurrentSet() { HasMap hasMap = (HasMap) this.beanFactory.getBean("concurrentSet"); - assertThat(hasMap.getConcurrentSet().size()).isEqualTo(3); + assertThat(hasMap.getConcurrentSet()).hasSize(3); assertThat(hasMap.getConcurrentSet().contains("bar")).isTrue(); TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); assertThat(hasMap.getConcurrentSet().contains(jenny)).isTrue(); @@ -292,31 +292,31 @@ public void testPopulatedConcurrentSet() { } @Test - public void testPopulatedIdentityMap() { + void testPopulatedIdentityMap() { HasMap hasMap = (HasMap) this.beanFactory.getBean("identityMap"); - assertThat(hasMap.getIdentityMap().size()).isEqualTo(2); + assertThat(hasMap.getIdentityMap()).hasSize(2); HashSet set = new HashSet(hasMap.getIdentityMap().keySet()); - assertThat(set.contains("foo")).isTrue(); - assertThat(set.contains("jenny")).isTrue(); + assertThat(set).contains("foo"); + assertThat(set).contains("jenny"); } @Test - public void testEmptyProps() { + void testEmptyProps() { HasMap hasMap = (HasMap) this.beanFactory.getBean("emptyProps"); - assertThat(hasMap.getProps().size()).isEqualTo(0); + assertThat(hasMap.getProps()).hasSize(0); assertThat(Properties.class).isEqualTo(hasMap.getProps().getClass()); } @Test - public void testPopulatedProps() { + void testPopulatedProps() { HasMap hasMap = (HasMap) this.beanFactory.getBean("props"); - assertThat(hasMap.getProps().size()).isEqualTo(2); + assertThat(hasMap.getProps()).hasSize(2); assertThat(hasMap.getProps().get("foo").equals("bar")).isTrue(); assertThat(hasMap.getProps().get("2").equals("TWO")).isTrue(); } @Test - public void testObjectArray() { + void testObjectArray() { HasMap hasMap = (HasMap) this.beanFactory.getBean("objectArray"); assertThat(hasMap.getObjectArray().length).isEqualTo(2); assertThat(hasMap.getObjectArray()[0].equals("one")).isTrue(); @@ -324,7 +324,7 @@ public void testObjectArray() { } @Test - public void testIntegerArray() { + void testIntegerArray() { HasMap hasMap = (HasMap) this.beanFactory.getBean("integerArray"); assertThat(hasMap.getIntegerArray().length).isEqualTo(3); assertThat(hasMap.getIntegerArray()[0]).isEqualTo(0); @@ -333,7 +333,7 @@ public void testIntegerArray() { } @Test - public void testClassArray() { + void testClassArray() { HasMap hasMap = (HasMap) this.beanFactory.getBean("classArray"); assertThat(hasMap.getClassArray().length).isEqualTo(2); assertThat(hasMap.getClassArray()[0].equals(String.class)).isTrue(); @@ -341,15 +341,15 @@ public void testClassArray() { } @Test - public void testClassList() { + void testClassList() { HasMap hasMap = (HasMap) this.beanFactory.getBean("classList"); - assertThat(hasMap.getClassList().size()).isEqualTo(2); + assertThat(hasMap.getClassList()).hasSize(2); assertThat(hasMap.getClassList().get(0).equals(String.class)).isTrue(); assertThat(hasMap.getClassList().get(1).equals(Exception.class)).isTrue(); } @Test - public void testProps() { + void testProps() { HasMap hasMap = (HasMap) this.beanFactory.getBean("props"); assertThat(hasMap.getProps()).hasSize(2); assertThat(hasMap.getProps().getProperty("foo")).isEqualTo("bar"); @@ -362,76 +362,55 @@ public void testProps() { } @Test - public void testListFactory() { + void testListFactory() { List list = (List) this.beanFactory.getBean("listFactory"); - assertThat(list instanceof LinkedList).isTrue(); - assertThat(list.size()).isEqualTo(2); - assertThat(list.get(0)).isEqualTo("bar"); - assertThat(list.get(1)).isEqualTo("jenny"); + assertThat(list).isInstanceOf(LinkedList.class).containsExactly("bar", "jenny"); } @Test - public void testPrototypeListFactory() { + void testPrototypeListFactory() { List list = (List) this.beanFactory.getBean("pListFactory"); - assertThat(list instanceof LinkedList).isTrue(); - assertThat(list.size()).isEqualTo(2); - assertThat(list.get(0)).isEqualTo("bar"); - assertThat(list.get(1)).isEqualTo("jenny"); + assertThat(list).isInstanceOf(LinkedList.class).containsExactly("bar", "jenny"); } @Test - public void testSetFactory() { + void testSetFactory() { Set set = (Set) this.beanFactory.getBean("setFactory"); - assertThat(set instanceof TreeSet).isTrue(); - assertThat(set.size()).isEqualTo(2); - assertThat(set.contains("bar")).isTrue(); - assertThat(set.contains("jenny")).isTrue(); + assertThat(set).isInstanceOf(TreeSet.class).containsOnly("bar", "jenny"); } @Test - public void testPrototypeSetFactory() { + void testPrototypeSetFactory() { Set set = (Set) this.beanFactory.getBean("pSetFactory"); - assertThat(set instanceof TreeSet).isTrue(); - assertThat(set.size()).isEqualTo(2); - assertThat(set.contains("bar")).isTrue(); - assertThat(set.contains("jenny")).isTrue(); + assertThat(set).isInstanceOf(TreeSet.class).containsOnly("bar", "jenny"); } @Test - public void testMapFactory() { + void testMapFactory() { Map map = (Map) this.beanFactory.getBean("mapFactory"); - assertThat(map instanceof TreeMap).isTrue(); - assertThat(map.size()).isEqualTo(2); - assertThat(map.get("foo")).isEqualTo("bar"); - assertThat(map.get("jen")).isEqualTo("jenny"); + assertThat(map).isInstanceOf(TreeMap.class).containsOnly( + entry("foo", "bar"), entry("jen", "jenny")); } @Test - public void testPrototypeMapFactory() { + void testPrototypeMapFactory() { Map map = (Map) this.beanFactory.getBean("pMapFactory"); - assertThat(map instanceof TreeMap).isTrue(); - assertThat(map.size()).isEqualTo(2); - assertThat(map.get("foo")).isEqualTo("bar"); - assertThat(map.get("jen")).isEqualTo("jenny"); + assertThat(map).isInstanceOf(TreeMap.class).containsOnly( + entry("foo", "bar"), entry("jen", "jenny")); } @Test - public void testChoiceBetweenSetAndMap() { + void testChoiceBetweenSetAndMap() { MapAndSet sam = (MapAndSet) this.beanFactory.getBean("setAndMap"); assertThat(sam.getObject() instanceof Map).as("Didn't choose constructor with Map argument").isTrue(); Map map = (Map) sam.getObject(); - assertThat(map).hasSize(3); - assertThat(map.get("key1")).isEqualTo("val1"); - assertThat(map.get("key2")).isEqualTo("val2"); - assertThat(map.get("key3")).isEqualTo("val3"); + assertThat(map).containsOnly(entry("key1", "val1"), entry("key2", "val2"), entry("key3", "val3")); } @Test - public void testEnumSetFactory() { + void testEnumSetFactory() { Set set = (Set) this.beanFactory.getBean("enumSetFactory"); - assertThat(set.size()).isEqualTo(2); - assertThat(set.contains("ONE")).isTrue(); - assertThat(set.contains("TWO")).isTrue(); + assertThat(set).containsOnly("ONE", "TWO"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReaderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReaderTests.java index c71bd28ace5f..5df2606b9268 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReaderTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.beans.factory.xml; +import java.lang.reflect.Field; import java.util.Arrays; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.xml.sax.InputSource; @@ -29,107 +31,102 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; -import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** + * Tests for {@link XmlBeanDefinitionReader}. + * * @author Rick Evans * @author Juergen Hoeller * @author Sam Brannen */ -public class XmlBeanDefinitionReaderTests { +class XmlBeanDefinitionReaderTests { + + private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + + private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry); + @Test - public void setParserClassSunnyDay() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - new XmlBeanDefinitionReader(registry).setDocumentReaderClass(DefaultBeanDefinitionDocumentReader.class); + void setReaderClass() { + assertThatNoException().isThrownBy(() -> reader.setDocumentReaderClass(DefaultBeanDefinitionDocumentReader.class)); } @Test - public void withOpenInputStream() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + void withInputStreamResourceWithoutExplicitValidationMode() { Resource resource = new InputStreamResource(getClass().getResourceAsStream("test.xml")); - assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> - new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource)); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> reader.loadBeanDefinitions(resource)); } @Test - public void withOpenInputStreamAndExplicitValidationMode() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + void withInputStreamResourceAndExplicitValidationMode() { Resource resource = new InputStreamResource(getClass().getResourceAsStream("test.xml")); - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry); reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_DTD); reader.loadBeanDefinitions(resource); - testBeanDefinitions(registry); + assertBeanDefinitions(registry); } @Test - public void withImport() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + void withImport() { Resource resource = new ClassPathResource("import.xml", getClass()); - new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource); - testBeanDefinitions(registry); + reader.loadBeanDefinitions(resource); + assertBeanDefinitions(registry); } @Test - public void withWildcardImport() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + void withWildcardImport() { Resource resource = new ClassPathResource("importPattern.xml", getClass()); - new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource); - testBeanDefinitions(registry); + reader.loadBeanDefinitions(resource); + assertBeanDefinitions(registry); } @Test - public void withInputSource() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + void withInputSourceWithoutExplicitValidationMode() { InputSource resource = new InputSource(getClass().getResourceAsStream("test.xml")); - assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> - new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource)); + assertThatExceptionOfType(BeanDefinitionStoreException.class) + .isThrownBy(() -> reader.loadBeanDefinitions(resource)) + .withMessageStartingWith("Unable to determine validation mode for [resource loaded through SAX InputSource]:"); } @Test - public void withInputSourceAndExplicitValidationMode() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + void withInputSourceAndExplicitValidationMode() { InputSource resource = new InputSource(getClass().getResourceAsStream("test.xml")); - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry); reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_DTD); reader.loadBeanDefinitions(resource); - testBeanDefinitions(registry); + assertBeanDefinitions(registry); } @Test - public void withFreshInputStream() { - SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + void withClassPathResource() { Resource resource = new ClassPathResource("test.xml", getClass()); - new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource); - testBeanDefinitions(registry); + reader.loadBeanDefinitions(resource); + assertBeanDefinitions(registry); } - private void testBeanDefinitions(BeanDefinitionRegistry registry) { + private void assertBeanDefinitions(BeanDefinitionRegistry registry) { assertThat(registry.getBeanDefinitionCount()).isEqualTo(24); assertThat(registry.getBeanDefinitionNames()).hasSize(24); - assertThat(Arrays.asList(registry.getBeanDefinitionNames()).contains("rod")).isTrue(); - assertThat(Arrays.asList(registry.getBeanDefinitionNames()).contains("aliased")).isTrue(); + assertThat(registry.getBeanDefinitionNames()).contains("rod", "aliased"); assertThat(registry.containsBeanDefinition("rod")).isTrue(); assertThat(registry.containsBeanDefinition("aliased")).isTrue(); assertThat(registry.getBeanDefinition("rod").getBeanClassName()).isEqualTo(TestBean.class.getName()); assertThat(registry.getBeanDefinition("aliased").getBeanClassName()).isEqualTo(TestBean.class.getName()); assertThat(registry.isAlias("youralias")).isTrue(); - String[] aliases = registry.getAliases("aliased"); - assertThat(aliases).hasSize(2); - assertThat(ObjectUtils.containsElement(aliases, "myalias")).isTrue(); - assertThat(ObjectUtils.containsElement(aliases, "youralias")).isTrue(); + assertThat(registry.getAliases("aliased")).containsExactly("myalias", "youralias"); } @Test - public void dtdValidationAutodetect() { + void dtdValidationAutodetect() { doTestValidation("validateWithDtd.xml"); } @Test - public void xsdValidationAutodetect() { + void xsdValidationAutodetect() { doTestValidation("validateWithXsd.xml"); } @@ -137,8 +134,42 @@ private void doTestValidation(String resourceName) { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); Resource resource = new ClassPathResource(resourceName, getClass()); new XmlBeanDefinitionReader(factory).loadBeanDefinitions(resource); - TestBean bean = (TestBean) factory.getBean("testBean"); - assertThat(bean).isNotNull(); + assertThat((TestBean) factory.getBean("testBean")).isNotNull(); + } + + @Test + void setValidationModeNameToUnsupportedValues() { + assertThatIllegalArgumentException().isThrownBy(() -> reader.setValidationModeName(null)); + assertThatIllegalArgumentException().isThrownBy(() -> reader.setValidationModeName(" ")); + assertThatIllegalArgumentException().isThrownBy(() -> reader.setValidationModeName("bogus")); + } + + /** + * This test effectively verifies that the internal 'constants' map is properly + * configured for all VALIDATION_ constants defined in {@link XmlBeanDefinitionReader}. + */ + @Test + void setValidationModeNameToAllSupportedValues() { + streamValidationModeConstants() + .map(Field::getName) + .forEach(name -> assertThatNoException().as(name).isThrownBy(() -> reader.setValidationModeName(name))); + } + + @Test + void setValidationMode() { + assertThatIllegalArgumentException().isThrownBy(() -> reader.setValidationMode(999)); + + assertThatNoException().isThrownBy(() -> reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE)); + assertThatNoException().isThrownBy(() -> reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_AUTO)); + assertThatNoException().isThrownBy(() -> reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_DTD)); + assertThatNoException().isThrownBy(() -> reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD)); + } + + + private static Stream streamValidationModeConstants() { + return Arrays.stream(XmlBeanDefinitionReader.class.getFields()) + .filter(ReflectionUtils::isPublicStaticFinal) + .filter(field -> field.getName().startsWith("VALIDATION_")); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java index c77c851a8ed8..0699edb4a58a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ * @since 09.11.2003 */ @SuppressWarnings({"rawtypes", "unchecked"}) -public class XmlListableBeanFactoryTests extends AbstractListableBeanFactoryTests { +class XmlListableBeanFactoryTests extends AbstractListableBeanFactoryTests { private DefaultListableBeanFactory parent; @@ -52,7 +52,7 @@ public class XmlListableBeanFactoryTests extends AbstractListableBeanFactoryTest @BeforeEach - public void setup() { + void setup() { parent = new DefaultListableBeanFactory(); Map map = new HashMap(); @@ -105,24 +105,24 @@ public void count() { } @Test - public void beanCount() { + void beanCount() { assertTestBeanCount(13); } @Test - public void lifecycleMethods() { + void lifecycleMethods() { LifecycleBean bean = (LifecycleBean) getBeanFactory().getBean("lifecycle"); bean.businessMethod(); } @Test - public void protectedLifecycleMethods() { + void protectedLifecycleMethods() { ProtectedLifecycleBean bean = (ProtectedLifecycleBean) getBeanFactory().getBean("protectedLifecycle"); bean.businessMethod(); } @Test - public void descriptionButNoProperties() { + void descriptionButNoProperties() { TestBean validEmpty = (TestBean) getBeanFactory().getBean("validEmptyWithDescription"); assertThat(validEmpty.getAge()).isZero(); } @@ -131,7 +131,7 @@ public void descriptionButNoProperties() { * Test that properties with name as well as id creating an alias up front. */ @Test - public void autoAliasing() { + void autoAliasing() { List beanNames = Arrays.asList(getListableBeanFactory().getBeanDefinitionNames()); TestBean tb1 = (TestBean) getBeanFactory().getBean("aliased"); @@ -191,7 +191,7 @@ public void autoAliasing() { } @Test - public void factoryNesting() { + void factoryNesting() { ITestBean father = (ITestBean) getBeanFactory().getBean("father"); assertThat(father).as("Bean from root context").isNotNull(); @@ -204,7 +204,7 @@ public void factoryNesting() { } @Test - public void factoryReferences() { + void factoryReferences() { DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); DummyReferencer ref = (DummyReferencer) getBeanFactory().getBean("factoryReferencer"); @@ -217,7 +217,7 @@ public void factoryReferences() { } @Test - public void prototypeReferences() { + void prototypeReferences() { // check that not broken by circular reference resolution mechanism DummyReferencer ref1 = (DummyReferencer) getBeanFactory().getBean("prototypeReferencer"); assertThat(ref1.getTestBean1()).as("Not referencing same bean twice").isNotSameAs(ref1.getTestBean2()); @@ -230,7 +230,7 @@ public void prototypeReferences() { } @Test - public void beanPostProcessor() { + void beanPostProcessor() { TestBean kerry = (TestBean) getBeanFactory().getBean("kerry"); TestBean kathy = (TestBean) getBeanFactory().getBean("kathy"); DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); @@ -242,7 +242,7 @@ public void beanPostProcessor() { } @Test - public void emptyValues() { + void emptyValues() { TestBean rod = (TestBean) getBeanFactory().getBean("rod"); TestBean kerry = (TestBean) getBeanFactory().getBean("kerry"); assertThat(rod.getTouchy()).as("Touchy is empty").isEqualTo(""); @@ -250,7 +250,7 @@ public void emptyValues() { } @Test - public void commentsAndCdataInValue() { + void commentsAndCdataInValue() { TestBean bean = (TestBean) getBeanFactory().getBean("commentsInValue"); assertThat(bean.getName()).as("Failed to handle comments and CDATA properly").isEqualTo("this is a "); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java index faa41995e6d5..0aa52baa1623 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,10 +31,10 @@ * @author Rob Harrop * @author Rick Evans */ -public class DefaultNamespaceHandlerResolverTests { +class DefaultNamespaceHandlerResolverTests { @Test - public void testResolvedMappedHandler() { + void testResolvedMappedHandler() { DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(getClass().getClassLoader()); NamespaceHandler handler = resolver.resolve("http://www.springframework.org/schema/util"); assertThat(handler).as("Handler should not be null.").isNotNull(); @@ -42,7 +42,7 @@ public void testResolvedMappedHandler() { } @Test - public void testResolvedMappedHandlerWithNoArgCtor() { + void testResolvedMappedHandlerWithNoArgCtor() { DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(); NamespaceHandler handler = resolver.resolve("http://www.springframework.org/schema/util"); assertThat(handler).as("Handler should not be null.").isNotNull(); @@ -50,25 +50,25 @@ public void testResolvedMappedHandlerWithNoArgCtor() { } @Test - public void testNonExistentHandlerClass() { + void testNonExistentHandlerClass() { String mappingPath = "org/springframework/beans/factory/xml/support/nonExistent.properties"; new DefaultNamespaceHandlerResolver(getClass().getClassLoader(), mappingPath); } @Test - public void testCtorWithNullClassLoaderArgument() { + void testCtorWithNullClassLoaderArgument() { // simply must not bail... new DefaultNamespaceHandlerResolver(null); } @Test - public void testCtorWithNullClassLoaderArgumentAndNullMappingLocationArgument() { + void testCtorWithNullClassLoaderArgumentAndNullMappingLocationArgument() { assertThatIllegalArgumentException().isThrownBy(() -> new DefaultNamespaceHandlerResolver(null, null)); } @Test - public void testCtorWithNonExistentMappingLocationArgument() { + void testCtorWithNonExistentMappingLocationArgument() { // simply must not bail; we don't want non-existent resources to result in an Exception new DefaultNamespaceHandlerResolver(null, "738trbc bobabloobop871"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java index 7b91fc60e64a..73dbf7124585 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,10 @@ * @author Juergen Hoeller * @since 06.03.2006 */ -public class BeanInfoTests { +class BeanInfoTests { @Test - public void testComplexObject() { + void testComplexObject() { ValueBean bean = new ValueBean(); BeanWrapper bw = new BeanWrapperImpl(bean); Integer value = 1; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java index ef5bdc87d761..1b74fba99358 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,16 +23,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for the {@link ByteArrayPropertyEditor} class. + * Tests for {@link ByteArrayPropertyEditor}. * * @author Rick Evans */ -public class ByteArrayPropertyEditorTests { +class ByteArrayPropertyEditorTests { private final PropertyEditor byteEditor = new ByteArrayPropertyEditor(); @Test - public void sunnyDaySetAsText() throws Exception { + void sunnyDaySetAsText() { final String text = "Hideous towns make me throw... up"; byteEditor.setAsText(text); @@ -46,7 +46,7 @@ public void sunnyDaySetAsText() throws Exception { } @Test - public void getAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + void getAsTextReturnsEmptyStringIfValueIsNull() { assertThat(byteEditor.getAsText()).isEmpty(); byteEditor.setAsText(null); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java index af09ae6c6e0d..e6dda7de2884 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,16 +23,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for the {@link CharArrayPropertyEditor} class. + * Tests for {@link CharArrayPropertyEditor}. * * @author Rick Evans */ -public class CharArrayPropertyEditorTests { +class CharArrayPropertyEditorTests { private final PropertyEditor charEditor = new CharArrayPropertyEditor(); @Test - public void sunnyDaySetAsText() throws Exception { + void sunnyDaySetAsText() { final String text = "Hideous towns make me throw... up"; charEditor.setAsText(text); @@ -46,7 +46,7 @@ public void sunnyDaySetAsText() throws Exception { } @Test - public void getAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + void getAsTextReturnsEmptyStringIfValueIsNull() { assertThat(charEditor.getAsText()).isEmpty(); charEditor.setAsText(null); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java index d5597923f735..bc4f6701ecd5 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,15 +26,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link CustomCollectionEditor} class. + * Tests for {@link CustomCollectionEditor}. * * @author Rick Evans * @author Chris Beams */ -public class CustomCollectionEditorTests { +class CustomCollectionEditorTests { @Test - public void testCtorWithNullCollectionType() { + void testCtorWithNullCollectionType() { assertThatIllegalArgumentException().isThrownBy(() -> new CustomCollectionEditor(null)); } @@ -47,46 +47,42 @@ public void testCtorWithNonCollectionType() { } @Test - public void testWithCollectionTypeThatDoesNotExposeAPublicNoArgCtor() { + void testWithCollectionTypeThatDoesNotExposeAPublicNoArgCtor() { CustomCollectionEditor editor = new CustomCollectionEditor(CollectionTypeWithNoNoArgCtor.class); assertThatIllegalArgumentException().isThrownBy(() -> editor.setValue("1")); } @Test - public void testSunnyDaySetValue() { + void testSunnyDaySetValue() { CustomCollectionEditor editor = new CustomCollectionEditor(ArrayList.class); editor.setValue(new int[] {0, 1, 2}); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof ArrayList).isTrue(); - List list = (List) value; - assertThat(list).as("There must be 3 elements in the converted collection").hasSize(3); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); + assertThat(value).isInstanceOf(ArrayList.class); + assertThat(value).asList().containsExactly(0, 1, 2); } @Test - public void testWhenTargetTypeIsExactlyTheCollectionInterfaceUsesFallbackCollectionType() { + void testWhenTargetTypeIsExactlyTheCollectionInterfaceUsesFallbackCollectionType() { CustomCollectionEditor editor = new CustomCollectionEditor(Collection.class); editor.setValue("0, 1, 2"); Collection value = (Collection) editor.getValue(); assertThat(value).isNotNull(); assertThat(value).as("There must be 1 element in the converted collection").hasSize(1); - assertThat(value.iterator().next()).isEqualTo("0, 1, 2"); + assertThat(value).singleElement().isEqualTo("0, 1, 2"); } @Test - public void testSunnyDaySetAsTextYieldsSingleValue() { + void testSunnyDaySetAsTextYieldsSingleValue() { CustomCollectionEditor editor = new CustomCollectionEditor(ArrayList.class); editor.setValue("0, 1, 2"); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof ArrayList).isTrue(); + assertThat(value).isInstanceOf(ArrayList.class); List list = (List) value; assertThat(list).as("There must be 1 element in the converted collection").hasSize(1); - assertThat(list.get(0)).isEqualTo("0, 1, 2"); + assertThat(list).singleElement().isEqualTo("0, 1, 2"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java index b744cd94aa99..f3dd380be7b1 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.beans.PropertyEditor; import java.beans.PropertyEditorSupport; -import java.beans.PropertyVetoException; import java.io.File; import java.math.BigDecimal; import java.math.BigInteger; @@ -55,7 +54,7 @@ import static org.assertj.core.api.Assertions.within; /** - * Unit tests for the various PropertyEditors in Spring. + * Tests for the various PropertyEditors in Spring. * * @author Juergen Hoeller * @author Rick Evans @@ -67,7 +66,7 @@ class CustomEditorTests { @Test - void testComplexObject() { + void complexObject() { TestBean tb = new TestBean(); String newName = "Rod"; String tbString = "Kerry_34"; @@ -85,7 +84,7 @@ void testComplexObject() { } @Test - void testComplexObjectWithOldValueAccess() { + void complexObjectWithOldValueAccess() { TestBean tb = new TestBean(); String newName = "Rod"; String tbString = "Kerry_34"; @@ -109,7 +108,7 @@ void testComplexObjectWithOldValueAccess() { } @Test - void testCustomEditorForSingleProperty() { + void customEditorForSingleProperty() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { @@ -127,7 +126,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorForAllStringProperties() { + void customEditorForAllStringProperties() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -145,7 +144,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorForSingleNestedProperty() { + void customEditorForSingleNestedProperty() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -164,7 +163,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorForAllNestedStringProperties() { + void customEditorForAllNestedStringProperties() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -183,7 +182,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testDefaultBooleanEditorForPrimitiveType() { + void defaultBooleanEditorForPrimitiveType() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -193,38 +192,38 @@ void testDefaultBooleanEditorForPrimitiveType() { bw.setPropertyValue("bool1", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool1"))).as("Correct bool1 value").isTrue(); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", " true "); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", " false "); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", "on"); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", "off"); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", "yes"); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", "no"); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", "1"); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", "0"); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); assertThatExceptionOfType(BeansException.class).isThrownBy(() -> bw.setPropertyValue("bool1", "argh")); } @Test - void testDefaultBooleanEditorForWrapperType() { + void defaultBooleanEditorForWrapperType() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -234,32 +233,32 @@ void testDefaultBooleanEditorForWrapperType() { bw.setPropertyValue("bool2", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "on"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "off"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "yes"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "no"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "1"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "0"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", ""); assertThat(tb.getBool2()).as("Correct bool2 value").isNull(); } @Test - void testCustomBooleanEditorWithAllowEmpty() { + void customBooleanEditorWithAllowEmpty() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(Boolean.class, new CustomBooleanEditor(true)); @@ -270,25 +269,25 @@ void testCustomBooleanEditorWithAllowEmpty() { bw.setPropertyValue("bool2", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "on"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "off"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "yes"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "no"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "1"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "0"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", ""); assertThat(bw.getPropertyValue("bool2")).as("Correct bool2 value").isNull(); @@ -296,7 +295,7 @@ void testCustomBooleanEditorWithAllowEmpty() { } @Test - void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() { + void customBooleanEditorWithSpecialTrueAndFalseStrings() { String trueString = "pechorin"; String falseString = "nash"; @@ -320,7 +319,7 @@ void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() { } @Test - void testDefaultNumberEditor() { + void defaultNumberEditor() { NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -364,7 +363,7 @@ void testDefaultNumberEditor() { } @Test - void testCustomNumberEditorWithoutAllowEmpty() { + void customNumberEditorWithoutAllowEmpty() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -421,7 +420,7 @@ void testCustomNumberEditorWithoutAllowEmpty() { } @Test - void testCustomNumberEditorWithAllowEmpty() { + void customNumberEditorWithAllowEmpty() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -445,7 +444,7 @@ void testCustomNumberEditorWithAllowEmpty() { } @Test - void testCustomNumberEditorWithFrenchBigDecimal() { + void customNumberEditorWithFrenchBigDecimal() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.FRENCH); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -462,14 +461,14 @@ void testCustomNumberEditorWithFrenchBigDecimal() { } @Test - void testParseShortGreaterThanMaxValueWithoutNumberFormat() { + void parseShortGreaterThanMaxValueWithoutNumberFormat() { CustomNumberEditor editor = new CustomNumberEditor(Short.class, true); assertThatExceptionOfType(NumberFormatException.class).as("greater than Short.MAX_VALUE + 1").isThrownBy(() -> editor.setAsText(String.valueOf(Short.MAX_VALUE + 1))); } @Test - void testByteArrayPropertyEditor() { + void byteArrayPropertyEditor() { PrimitiveArrayBean bean = new PrimitiveArrayBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.setPropertyValue("byteArray", "myvalue"); @@ -477,7 +476,7 @@ void testByteArrayPropertyEditor() { } @Test - void testCharArrayPropertyEditor() { + void charArrayPropertyEditor() { PrimitiveArrayBean bean = new PrimitiveArrayBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.setPropertyValue("charArray", "myvalue"); @@ -485,7 +484,7 @@ void testCharArrayPropertyEditor() { } @Test - void testCharacterEditor() { + void characterEditor() { CharBean cb = new CharBean(); BeanWrapper bw = new BeanWrapperImpl(cb); @@ -507,7 +506,7 @@ void testCharacterEditor() { } @Test - void testCharacterEditorWithAllowEmpty() { + void characterEditorWithAllowEmpty() { CharBean cb = new CharBean(); BeanWrapper bw = new BeanWrapperImpl(cb); bw.registerCustomEditor(Character.class, new CharacterEditor(true)); @@ -529,14 +528,14 @@ void testCharacterEditorWithAllowEmpty() { } @Test - void testCharacterEditorSetAsTextWithStringLongerThanOneCharacter() { + void characterEditorSetAsTextWithStringLongerThanOneCharacter() { PropertyEditor charEditor = new CharacterEditor(false); assertThatIllegalArgumentException().isThrownBy(() -> charEditor.setAsText("ColdWaterCanyon")); } @Test - void testCharacterEditorGetAsTextReturnsEmptyStringIfValueIsNull() { + void characterEditorGetAsTextReturnsEmptyStringIfValueIsNull() { PropertyEditor charEditor = new CharacterEditor(false); assertThat(charEditor.getAsText()).isEmpty(); charEditor = new CharacterEditor(true); @@ -549,14 +548,14 @@ void testCharacterEditorGetAsTextReturnsEmptyStringIfValueIsNull() { } @Test - void testCharacterEditorSetAsTextWithNullNotAllowingEmptyAsNull() { + void characterEditorSetAsTextWithNullNotAllowingEmptyAsNull() { PropertyEditor charEditor = new CharacterEditor(false); assertThatIllegalArgumentException().isThrownBy(() -> charEditor.setAsText(null)); } @Test - void testClassEditor() { + void classEditor() { PropertyEditor classEditor = new ClassEditor(); classEditor.setAsText(TestBean.class.getName()); assertThat(classEditor.getValue()).isEqualTo(TestBean.class); @@ -571,14 +570,14 @@ void testClassEditor() { } @Test - void testClassEditorWithNonExistentClass() { + void classEditorWithNonExistentClass() { PropertyEditor classEditor = new ClassEditor(); assertThatIllegalArgumentException().isThrownBy(() -> classEditor.setAsText("hairdresser.on.Fire")); } @Test - void testClassEditorWithArray() { + void classEditorWithArray() { PropertyEditor classEditor = new ClassEditor(); classEditor.setAsText("org.springframework.beans.testfixture.beans.TestBean[]"); assertThat(classEditor.getValue()).isEqualTo(TestBean[].class); @@ -589,7 +588,7 @@ void testClassEditorWithArray() { * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays */ @Test - void testGetAsTextWithTwoDimensionalArray() { + void getAsTextWithTwoDimensionalArray() { String[][] chessboard = new String[8][8]; ClassEditor editor = new ClassEditor(); editor.setValue(chessboard.getClass()); @@ -600,7 +599,7 @@ void testGetAsTextWithTwoDimensionalArray() { * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays */ @Test - void testGetAsTextWithRidiculousMultiDimensionalArray() { + void getAsTextWithRidiculousMultiDimensionalArray() { String[][][][][] ridiculousChessboard = new String[8][4][0][1][3]; ClassEditor editor = new ClassEditor(); editor.setValue(ridiculousChessboard.getClass()); @@ -608,7 +607,7 @@ void testGetAsTextWithRidiculousMultiDimensionalArray() { } @Test - void testFileEditor() { + void fileEditor() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("file:myfile.txt"); assertThat(fileEditor.getValue()).isEqualTo(new File("myfile.txt")); @@ -616,7 +615,7 @@ void testFileEditor() { } @Test - void testFileEditorWithRelativePath() { + void fileEditorWithRelativePath() { PropertyEditor fileEditor = new FileEditor(); try { fileEditor.setAsText("myfile.txt"); @@ -628,7 +627,7 @@ void testFileEditorWithRelativePath() { } @Test - void testFileEditorWithAbsolutePath() { + void fileEditorWithAbsolutePath() { PropertyEditor fileEditor = new FileEditor(); // testing on Windows if (new File("C:/myfile.txt").isAbsolute()) { @@ -643,18 +642,22 @@ void testFileEditorWithAbsolutePath() { } @Test - void testLocaleEditor() { + void localeEditor() { PropertyEditor localeEditor = new LocaleEditor(); localeEditor.setAsText("en_CA"); assertThat(localeEditor.getValue()).isEqualTo(Locale.CANADA); assertThat(localeEditor.getAsText()).isEqualTo("en_CA"); + localeEditor = new LocaleEditor(); + localeEditor.setAsText("zh-Hans"); + assertThat(localeEditor.getValue()).isEqualTo(Locale.forLanguageTag("zh-Hans")); + localeEditor = new LocaleEditor(); assertThat(localeEditor.getAsText()).isEmpty(); } @Test - void testPatternEditor() { + void patternEditor() { final String REGEX = "a.*"; PropertyEditor patternEditor = new PatternEditor(); @@ -671,7 +674,7 @@ void testPatternEditor() { } @Test - void testCustomBooleanEditor() { + void customBooleanEditor() { CustomBooleanEditor editor = new CustomBooleanEditor(false); editor.setAsText("true"); @@ -691,7 +694,7 @@ void testCustomBooleanEditor() { } @Test - void testCustomBooleanEditorWithEmptyAsNull() { + void customBooleanEditorWithEmptyAsNull() { CustomBooleanEditor editor = new CustomBooleanEditor(true); editor.setAsText("true"); @@ -708,7 +711,7 @@ void testCustomBooleanEditorWithEmptyAsNull() { } @Test - void testCustomDateEditor() { + void customDateEditor() { CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), false); editor.setValue(null); assertThat(editor.getValue()).isNull(); @@ -716,7 +719,7 @@ void testCustomDateEditor() { } @Test - void testCustomDateEditorWithEmptyAsNull() { + void customDateEditorWithEmptyAsNull() { CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true); editor.setValue(null); assertThat(editor.getValue()).isNull(); @@ -724,7 +727,7 @@ void testCustomDateEditorWithEmptyAsNull() { } @Test - void testCustomDateEditorWithExactDateLength() { + void customDateEditorWithExactDateLength() { int maxLength = 10; String validDate = "01/01/2005"; String invalidDate = "01/01/05"; @@ -740,7 +743,7 @@ void testCustomDateEditorWithExactDateLength() { } @Test - void testCustomNumberEditor() { + void customNumberEditor() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); editor.setAsText("5"); assertThat(editor.getValue()).isEqualTo(5); @@ -751,14 +754,14 @@ void testCustomNumberEditor() { } @Test - void testCustomNumberEditorWithHex() { + void customNumberEditorWithHex() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); editor.setAsText("0x" + Integer.toHexString(64)); assertThat(editor.getValue()).isEqualTo(64); } @Test - void testCustomNumberEditorWithEmptyAsNull() { + void customNumberEditorWithEmptyAsNull() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, true); editor.setAsText("5"); assertThat(editor.getValue()).isEqualTo(5); @@ -772,7 +775,7 @@ void testCustomNumberEditorWithEmptyAsNull() { } @Test - void testStringTrimmerEditor() { + void stringTrimmerEditor() { StringTrimmerEditor editor = new StringTrimmerEditor(false); editor.setAsText("test"); assertThat(editor.getValue()).isEqualTo("test"); @@ -790,7 +793,7 @@ void testStringTrimmerEditor() { } @Test - void testStringTrimmerEditorWithEmptyAsNull() { + void stringTrimmerEditorWithEmptyAsNull() { StringTrimmerEditor editor = new StringTrimmerEditor(true); editor.setAsText("test"); assertThat(editor.getValue()).isEqualTo("test"); @@ -806,7 +809,7 @@ void testStringTrimmerEditorWithEmptyAsNull() { } @Test - void testStringTrimmerEditorWithCharsToDelete() { + void stringTrimmerEditorWithCharsToDelete() { StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", false); editor.setAsText("te\ns\ft"); assertThat(editor.getValue()).isEqualTo("test"); @@ -822,7 +825,7 @@ void testStringTrimmerEditorWithCharsToDelete() { } @Test - void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { + void stringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", true); editor.setAsText("te\ns\ft"); assertThat(editor.getValue()).isEqualTo("test"); @@ -838,7 +841,7 @@ void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { } @Test - void testIndexedPropertiesWithCustomEditorForType() { + void indexedPropertiesWithCustomEditorForType() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -891,7 +894,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testIndexedPropertiesWithCustomEditorForProperty() { + void indexedPropertiesWithCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() { @@ -958,7 +961,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testIndexedPropertiesWithIndividualCustomEditorForProperty() { + void indexedPropertiesWithIndividualCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, "array[0].name", new PropertyEditorSupport() { @@ -1043,7 +1046,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testNestedIndexedPropertiesWithCustomEditorForProperty() { + void nestedIndexedPropertiesWithCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(); TestBean tb0 = bean.getArray()[0]; TestBean tb1 = bean.getArray()[1]; @@ -1127,7 +1130,7 @@ public String getAsText() { } @Test - void testNestedIndexedPropertiesWithIndexedCustomEditorForProperty() { + void nestedIndexedPropertiesWithIndexedCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(); TestBean tb0 = bean.getArray()[0]; TestBean tb1 = bean.getArray()[1]; @@ -1178,7 +1181,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testIndexedPropertiesWithDirectAccessAndPropertyEditors() { + void indexedPropertiesWithDirectAccessAndPropertyEditors() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() { @@ -1232,7 +1235,7 @@ public String getAsText() { } @Test - void testIndexedPropertiesWithDirectAccessAndSpecificPropertyEditors() { + void indexedPropertiesWithDirectAccessAndSpecificPropertyEditors() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(TestBean.class, "array[0]", new PropertyEditorSupport() { @@ -1319,7 +1322,7 @@ public String getAsText() { } @Test - void testIndexedPropertiesWithListPropertyEditor() { + void indexedPropertiesWithListPropertyEditor() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(List.class, "list", new PropertyEditorSupport() { @@ -1333,11 +1336,11 @@ public void setAsText(String text) throws IllegalArgumentException { bw.setPropertyValue("list", "1"); assertThat(((TestBean) bean.getList().get(0)).getName()).isEqualTo("list1"); bw.setPropertyValue("list[0]", "test"); - assertThat(bean.getList().get(0)).isEqualTo("test"); + assertThat(bean.getList()).singleElement().isEqualTo("test"); } @Test - void testConversionToOldCollections() throws PropertyVetoException { + void conversionToOldCollections() { OldCollectionsBean tb = new OldCollectionsBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(Vector.class, new CustomCollectionEditor(Vector.class)); @@ -1345,8 +1348,8 @@ void testConversionToOldCollections() throws PropertyVetoException { bw.setPropertyValue("vector", new String[] {"a", "b"}); assertThat(tb.getVector()).hasSize(2); - assertThat(tb.getVector().get(0)).isEqualTo("a"); - assertThat(tb.getVector().get(1)).isEqualTo("b"); + assertThat(tb.getVector()).element(0).isEqualTo("a"); + assertThat(tb.getVector()).element(1).isEqualTo("b"); bw.setPropertyValue("hashtable", Collections.singletonMap("foo", "bar")); assertThat(tb.getHashtable()).hasSize(1); @@ -1354,7 +1357,7 @@ void testConversionToOldCollections() throws PropertyVetoException { } @Test - void testUninitializedArrayPropertyWithCustomEditor() { + void uninitializedArrayPropertyWithCustomEditor() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); PropertyEditor pe = new CustomNumberEditor(Integer.class, true); @@ -1362,7 +1365,7 @@ void testUninitializedArrayPropertyWithCustomEditor() { TestBean tb = new TestBean(); bw.setPropertyValue("list", new ArrayList<>()); bw.setPropertyValue("list[0]", tb); - assertThat(bean.getList().get(0)).isEqualTo(tb); + assertThat(bean.getList()).element(0).isEqualTo(tb); assertThat(bw.findCustomEditor(int.class, "list.age")).isEqualTo(pe); assertThat(bw.findCustomEditor(null, "list.age")).isEqualTo(pe); assertThat(bw.findCustomEditor(int.class, "list[0].age")).isEqualTo(pe); @@ -1370,7 +1373,7 @@ void testUninitializedArrayPropertyWithCustomEditor() { } @Test - void testArrayToArrayConversion() throws PropertyVetoException { + void arrayToArrayConversion() { IndexedTestBean tb = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { @@ -1386,7 +1389,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testArrayToStringConversion() throws PropertyVetoException { + void arrayToStringConversion() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -1400,7 +1403,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testClassArrayEditorSunnyDay() { + void classArrayEditorSunnyDay() { ClassArrayEditor classArrayEditor = new ClassArrayEditor(); classArrayEditor.setAsText("java.lang.String,java.util.HashMap"); Class[] classes = (Class[]) classArrayEditor.getValue(); @@ -1413,7 +1416,7 @@ void testClassArrayEditorSunnyDay() { } @Test - void testClassArrayEditorSunnyDayWithArrayTypes() { + void classArrayEditorSunnyDayWithArrayTypes() { ClassArrayEditor classArrayEditor = new ClassArrayEditor(); classArrayEditor.setAsText("java.lang.String[],java.util.Map[],int[],float[][][]"); Class[] classes = (Class[]) classArrayEditor.getValue(); @@ -1428,7 +1431,7 @@ void testClassArrayEditorSunnyDayWithArrayTypes() { } @Test - void testClassArrayEditorSetAsTextWithNull() { + void classArrayEditorSetAsTextWithNull() { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText(null); assertThat(editor.getValue()).isNull(); @@ -1436,7 +1439,7 @@ void testClassArrayEditorSetAsTextWithNull() { } @Test - void testClassArrayEditorSetAsTextWithEmptyString() { + void classArrayEditorSetAsTextWithEmptyString() { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText(""); assertThat(editor.getValue()).isNull(); @@ -1444,7 +1447,7 @@ void testClassArrayEditorSetAsTextWithEmptyString() { } @Test - void testClassArrayEditorSetAsTextWithWhitespaceString() { + void classArrayEditorSetAsTextWithWhitespaceString() { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText("\n"); assertThat(editor.getValue()).isNull(); @@ -1452,7 +1455,7 @@ void testClassArrayEditorSetAsTextWithWhitespaceString() { } @Test - void testCharsetEditor() { + void charsetEditor() { CharsetEditor editor = new CharsetEditor(); String name = "UTF-8"; editor.setAsText(name); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java index 3076977e9ec4..84d6eff38126 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,54 +31,64 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class FileEditorTests { +class FileEditorTests { @Test - public void testClasspathFileName() { + void testClasspathFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).exists(); } @Test - public void testWithNonExistentResource() { - PropertyEditor propertyEditor = new FileEditor(); + void testWithNonExistentResource() { + PropertyEditor fileEditor = new FileEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:no_way_this_file_is_found.doc")); + fileEditor.setAsText("classpath:no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentFile() { + void testWithNonExistentFile() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("file:no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); } @Test - public void testAbsoluteFileName() { + void testAbsoluteFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); } @Test - public void testUnqualifiedFileNameFound() { + void testCurrentDirectory() { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("file:."); + Object value = fileEditor.getValue(); + assertThat(value).isInstanceOf(File.class); + File file = (File) value; + assertThat(file).isEqualTo(new File(".")); + } + + @Test + void testUnqualifiedFileNameFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).exists(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); @@ -86,13 +96,13 @@ public void testUnqualifiedFileNameFound() { } @Test - public void testUnqualifiedFileNameNotFound() { + void testUnqualifiedFileNameNotFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java index d0a3e676ecf6..fe31766d9bb3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,21 +27,21 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link InputStreamEditor} class. + * Tests for {@link InputStreamEditor}. * * @author Rick Evans * @author Chris Beams */ -public class InputStreamEditorTests { +class InputStreamEditorTests { @Test - public void testCtorWithNullResourceEditor() { + void testCtorWithNullResourceEditor() { assertThatIllegalArgumentException().isThrownBy(() -> new InputStreamEditor(null)); } @Test - public void testSunnyDay() throws IOException { + void testSunnyDay() throws IOException { InputStream stream = null; try { String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + @@ -50,7 +50,7 @@ public void testSunnyDay() throws IOException { editor.setAsText(resource); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof InputStream).isTrue(); + assertThat(value).isInstanceOf(InputStream.class); stream = (InputStream) value; assertThat(stream.available()).isGreaterThan(0); } @@ -62,14 +62,14 @@ public void testSunnyDay() throws IOException { } @Test - public void testWhenResourceDoesNotExist() { + void testWhenResourceDoesNotExist() { InputStreamEditor editor = new InputStreamEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText("classpath:bingo!")); } @Test - public void testGetAsTextReturnsNullByDefault() { + void testGetAsTextReturnsNullByDefault() { assertThat(new InputStreamEditor().getAsText()).isNull(); String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java index d55cc18d48a4..ed4058b4fe53 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.beans.PropertyEditor; import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; import org.junit.jupiter.api.Test; @@ -31,10 +32,10 @@ * @author Juergen Hoeller * @since 4.3.2 */ -public class PathEditorTests { +class PathEditorTests { @Test - public void testClasspathPathName() { + void testClasspathPathName() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); @@ -45,14 +46,14 @@ public void testClasspathPathName() { } @Test - public void testWithNonExistentResource() { - PropertyEditor propertyEditor = new PathEditor(); + void testWithNonExistentResource() { + PropertyEditor pathEditor = new PathEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); + pathEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentPath() { + void testWithNonExistentPath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("file:/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -62,7 +63,7 @@ public void testWithNonExistentPath() { } @Test - public void testAbsolutePath() { + void testAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -72,7 +73,7 @@ public void testAbsolutePath() { } @Test - public void testWindowsAbsolutePath() { + void testWindowsAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("C:\\no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -82,7 +83,7 @@ public void testWindowsAbsolutePath() { } @Test - public void testWindowsAbsoluteFilePath() { + void testWindowsAbsoluteFilePath() { PropertyEditor pathEditor = new PathEditor(); try { pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc"); @@ -99,7 +100,17 @@ public void testWindowsAbsoluteFilePath() { } @Test - public void testUnqualifiedPathNameFound() { + void testCurrentDirectory() { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("file:."); + Object value = pathEditor.getValue(); + assertThat(value).isInstanceOf(Path.class); + Path path = (Path) value; + assertThat(path).isEqualTo(Paths.get(".")); + } + + @Test + void testUnqualifiedPathNameFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; @@ -117,7 +128,7 @@ public void testUnqualifiedPathNameFound() { } @Test - public void testUnqualifiedPathNameNotFound() { + void testUnqualifiedPathNameNotFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java index b3ec8b4be28b..3671fa3d0281 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; /** @@ -32,10 +33,10 @@ * @author Juergen Hoeller * @author Rick Evans */ -public class PropertiesEditorTests { +class PropertiesEditorTests { @Test - public void oneProperty() { + void oneProperty() { String s = "foo=bar"; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); @@ -45,7 +46,7 @@ public void oneProperty() { } @Test - public void twoProperties() { + void twoProperties() { String s = "foo=bar with whitespace\n" + "me=mi"; PropertiesEditor pe= new PropertiesEditor(); @@ -57,10 +58,11 @@ public void twoProperties() { } @Test - public void handlesEqualsInValue() { - String s = "foo=bar\n" + - "me=mi\n" + - "x=y=z"; + void handlesEqualsInValue() { + String s = """ + foo=bar + me=mi + x=y=z"""; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); Properties p = (Properties) pe.getValue(); @@ -71,7 +73,7 @@ public void handlesEqualsInValue() { } @Test - public void handlesEmptyProperty() { + void handlesEmptyProperty() { String s = "foo=bar\nme=mi\nx="; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); @@ -83,7 +85,7 @@ public void handlesEmptyProperty() { } @Test - public void handlesEmptyPropertyWithoutEquals() { + void handlesEmptyPropertyWithoutEquals() { String s = "foo\nme=mi\nx=x"; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); @@ -97,13 +99,15 @@ public void handlesEmptyPropertyWithoutEquals() { * Comments begin with # */ @Test - public void ignoresCommentLinesAndEmptyLines() { - String s = "#Ignore this comment\n" + - "foo=bar\n" + - "#Another=comment more junk /\n" + - "me=mi\n" + - "x=x\n" + - "\n"; + void ignoresCommentLinesAndEmptyLines() { + String s = """ + #Ignore this comment + foo=bar + #Another=comment more junk / + me=mi + x=x + + """; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); Properties p = (Properties) pe.getValue(); @@ -119,7 +123,7 @@ public void ignoresCommentLinesAndEmptyLines() { * still ignored: The standard syntax doesn't allow this on JDK 1.3. */ @Test - public void ignoresLeadingSpacesAndTabs() { + void ignoresLeadingSpacesAndTabs() { String s = " #Ignore this comment\n" + "\t\tfoo=bar\n" + "\t#Another comment more junk \n" + @@ -129,13 +133,12 @@ public void ignoresLeadingSpacesAndTabs() { PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); Properties p = (Properties) pe.getValue(); - assertThat(p.size()).as("contains 3 entries, not " + p.size()).isEqualTo(3); - assertThat(p.get("foo").equals("bar")).as("foo is bar").isTrue(); - assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + assertThat(p).contains(entry("foo", "bar"), entry("me", "mi")); + assertThat(p).hasSize(3); } @Test - public void nullValue() { + void nullValue() { PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(null); Properties p = (Properties) pe.getValue(); @@ -143,7 +146,7 @@ public void nullValue() { } @Test - public void emptyString() { + void emptyString() { PropertiesEditor pe = new PropertiesEditor(); pe.setAsText(""); Properties p = (Properties) pe.getValue(); @@ -151,7 +154,7 @@ public void emptyString() { } @Test - public void usingMapAsValueSource() { + void usingMapAsValueSource() { Map map = new HashMap<>(); map.put("one", "1"); map.put("two", "2"); @@ -160,7 +163,7 @@ public void usingMapAsValueSource() { pe.setValue(map); Object value = pe.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof Properties).isTrue(); + assertThat(value).isInstanceOf(Properties.class); Properties props = (Properties) value; assertThat(props).hasSize(3); assertThat(props.getProperty("one")).isEqualTo("1"); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java index 79e48a5db085..3425e825bb13 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,21 +27,21 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link ReaderEditor} class. + * Tests for {@link ReaderEditor}. * * @author Juergen Hoeller * @since 4.2 */ -public class ReaderEditorTests { +class ReaderEditorTests { @Test - public void testCtorWithNullResourceEditor() { + void testCtorWithNullResourceEditor() { assertThatIllegalArgumentException().isThrownBy(() -> new ReaderEditor(null)); } @Test - public void testSunnyDay() throws IOException { + void testSunnyDay() throws IOException { Reader reader = null; try { String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + @@ -50,7 +50,7 @@ public void testSunnyDay() throws IOException { editor.setAsText(resource); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof Reader).isTrue(); + assertThat(value).isInstanceOf(Reader.class); reader = (Reader) value; assertThat(reader.ready()).isTrue(); } @@ -62,7 +62,7 @@ public void testSunnyDay() throws IOException { } @Test - public void testWhenResourceDoesNotExist() { + void testWhenResourceDoesNotExist() { String resource = "classpath:bingo!"; ReaderEditor editor = new ReaderEditor(); assertThatIllegalArgumentException().isThrownBy(() -> @@ -70,7 +70,7 @@ public void testWhenResourceDoesNotExist() { } @Test - public void testGetAsTextReturnsNullByDefault() { + void testGetAsTextReturnsNullByDefault() { assertThat(new ReaderEditor().getAsText()).isNull(); String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java index 4cbdef1ddc4f..7fb84bca01c8 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,12 +24,12 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link ResourceBundleEditor} class. + * Tests for {@link ResourceBundleEditor}. * * @author Rick Evans * @author Chris Beams */ -public class ResourceBundleEditorTests { +class ResourceBundleEditorTests { private static final String BASE_NAME = ResourceBundleEditorTests.class.getName(); @@ -37,7 +37,7 @@ public class ResourceBundleEditorTests { @Test - public void testSetAsTextWithJustBaseName() { + void testSetAsTextWithJustBaseName() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME); Object value = editor.getValue(); @@ -49,7 +49,7 @@ public void testSetAsTextWithJustBaseName() { } @Test - public void testSetAsTextWithBaseNameThatEndsInDefaultSeparator() { + void testSetAsTextWithBaseNameThatEndsInDefaultSeparator() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "_"); Object value = editor.getValue(); @@ -61,7 +61,7 @@ public void testSetAsTextWithBaseNameThatEndsInDefaultSeparator() { } @Test - public void testSetAsTextWithBaseNameAndLanguageCode() { + void testSetAsTextWithBaseNameAndLanguageCode() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "Lang" + "_en"); Object value = editor.getValue(); @@ -73,7 +73,7 @@ public void testSetAsTextWithBaseNameAndLanguageCode() { } @Test - public void testSetAsTextWithBaseNameLanguageAndCountryCode() { + void testSetAsTextWithBaseNameLanguageAndCountryCode() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "LangCountry" + "_en_GB"); Object value = editor.getValue(); @@ -85,7 +85,7 @@ public void testSetAsTextWithBaseNameLanguageAndCountryCode() { } @Test - public void testSetAsTextWithTheKitchenSink() { + void testSetAsTextWithTheKitchenSink() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "LangCountryDialect" + "_en_GB_GLASGOW"); Object value = editor.getValue(); @@ -97,28 +97,28 @@ public void testSetAsTextWithTheKitchenSink() { } @Test - public void testSetAsTextWithNull() { + void testSetAsTextWithNull() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText(null)); } @Test - public void testSetAsTextWithEmptyString() { + void testSetAsTextWithEmptyString() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText("")); } @Test - public void testSetAsTextWithWhiteSpaceString() { + void testSetAsTextWithWhiteSpaceString() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText(" ")); } @Test - public void testSetAsTextWithJustSeparatorString() { + void testSetAsTextWithJustSeparatorString() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText("_")); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java index 4d215e015661..18c28234c21f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,75 +29,75 @@ * @author Juergen Hoeller * @author Arjen Poutsma */ -public class URIEditorTests { +class URIEditorTests { @Test - public void standardURI() { + void standardURI() { doTestURI("mailto:juergen.hoeller@interface21.com"); } @Test - public void withNonExistentResource() { + void withNonExistentResource() { doTestURI("gonna:/freak/in/the/morning/freak/in/the.evening"); } @Test - public void standardURL() { + void standardURL() { doTestURI("https://www.springframework.org"); } @Test - public void standardURLWithFragment() { + void standardURLWithFragment() { doTestURI("https://www.springframework.org#1"); } @Test - public void standardURLWithWhitespace() { + void standardURLWithWhitespace() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText(" https://www.springframework.org "); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uri.toString()).isEqualTo("https://www.springframework.org"); } @Test - public void classpathURL() { + void classpathURL() { PropertyEditor uriEditor = new URIEditor(getClass().getClassLoader()); uriEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.getScheme()).doesNotStartWith("classpath"); } @Test - public void classpathURLWithWhitespace() { + void classpathURLWithWhitespace() { PropertyEditor uriEditor = new URIEditor(getClass().getClassLoader()); uriEditor.setAsText(" classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class "); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.getScheme()).doesNotStartWith("classpath"); } @Test - public void classpathURLAsIs() { + void classpathURLAsIs() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText("classpath:test.txt"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.getScheme()).startsWith("classpath"); } @Test - public void setAsTextWithNull() { + void setAsTextWithNull() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText(null); assertThat(uriEditor.getValue()).isNull(); @@ -105,28 +105,28 @@ public void setAsTextWithNull() { } @Test - public void getAsTextReturnsEmptyStringIfValueNotSet() { + void getAsTextReturnsEmptyStringIfValueNotSet() { PropertyEditor uriEditor = new URIEditor(); assertThat(uriEditor.getAsText()).isEmpty(); } @Test - public void encodeURI() { + void encodeURI() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText("https://example.com/spaces and \u20AC"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.toASCIIString()).isEqualTo("https://example.com/spaces%20and%20%E2%82%AC"); } @Test - public void encodeAlreadyEncodedURI() { + void encodeAlreadyEncodedURI() { PropertyEditor uriEditor = new URIEditor(false); uriEditor.setAsText("https://example.com/spaces%20and%20%E2%82%AC"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.toASCIIString()).isEqualTo("https://example.com/spaces%20and%20%E2%82%AC"); @@ -137,7 +137,7 @@ private void doTestURI(String uriSpec) { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText(uriSpec); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uri.toString()).isEqualTo(uriSpec); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java index ba96e1b9ee83..4b63b6e7f60b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,55 +30,55 @@ * @author Rick Evans * @author Chris Beams */ -public class URLEditorTests { +class URLEditorTests { @Test - public void testCtorWithNullResourceEditor() { + void testCtorWithNullResourceEditor() { assertThatIllegalArgumentException().isThrownBy(() -> new URLEditor(null)); } @Test - public void testStandardURI() { + void testStandardURI() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText("mailto:juergen.hoeller@interface21.com"); Object value = urlEditor.getValue(); - assertThat(value instanceof URL).isTrue(); + assertThat(value).isInstanceOf(URL.class); URL url = (URL) value; assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); } @Test - public void testStandardURL() { + void testStandardURL() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText("https://www.springframework.org"); Object value = urlEditor.getValue(); - assertThat(value instanceof URL).isTrue(); + assertThat(value).isInstanceOf(URL.class); URL url = (URL) value; assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); } @Test - public void testClasspathURL() { + void testClasspathURL() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = urlEditor.getValue(); - assertThat(value instanceof URL).isTrue(); + assertThat(value).isInstanceOf(URL.class); URL url = (URL) value; assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); assertThat(url.getProtocol()).doesNotStartWith("classpath"); } @Test - public void testWithNonExistentResource() { + void testWithNonExistentResource() { PropertyEditor urlEditor = new URLEditor(); assertThatIllegalArgumentException().isThrownBy(() -> urlEditor.setAsText("gonna:/freak/in/the/morning/freak/in/the.evening")); } @Test - public void testSetAsTextWithNull() { + void testSetAsTextWithNull() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText(null); assertThat(urlEditor.getValue()).isNull(); @@ -86,7 +86,7 @@ public void testSetAsTextWithNull() { } @Test - public void testGetAsTextReturnsEmptyStringIfValueNotSet() { + void testGetAsTextReturnsEmptyStringIfValueNotSet() { PropertyEditor urlEditor = new URLEditor(); assertThat(urlEditor.getAsText()).isEmpty(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java index 876233fb3f2a..110ea9979746 100644 --- a/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ * @author Chris Beams * @since 20.05.2003 */ -public class PagedListHolderTests { +class PagedListHolderTests { @Test @SuppressWarnings({ "rawtypes", "unchecked" }) diff --git a/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java b/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java index aac0b2097b0d..0bf4a4758eda 100644 --- a/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,15 +23,15 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link PropertyComparator}. + * Tests for {@link PropertyComparator}. * * @author Keith Donald * @author Chris Beams */ -public class PropertyComparatorTests { +class PropertyComparatorTests { @Test - public void testPropertyComparator() { + void testPropertyComparator() { Dog dog = new Dog(); dog.setNickName("mace"); @@ -45,7 +45,7 @@ public void testPropertyComparator() { } @Test - public void testPropertyComparatorNulls() { + void testPropertyComparatorNulls() { Dog dog = new Dog(); Dog dog2 = new Dog(); PropertyComparator c = new PropertyComparator<>("nickName", false, true); @@ -53,7 +53,7 @@ public void testPropertyComparatorNulls() { } @Test - public void testChainedComparators() { + void testChainedComparators() { Comparator c = new PropertyComparator<>("lastName", false, true); Dog dog1 = new Dog(); @@ -74,7 +74,7 @@ public void testChainedComparators() { } @Test - public void testChainedComparatorsReversed() { + void testChainedComparatorsReversed() { Comparator c = (new PropertyComparator("lastName", false, true)). thenComparing(new PropertyComparator<>("firstName", false, true)); diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt index 40ba3cdbf5e0..5b8378a49533 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt @@ -90,6 +90,74 @@ class BeanUtilsKotlinTests { BeanUtils.instantiateClass(PrivateClass::class.java.getDeclaredConstructor()) } + @Test + fun `Instantiate value class`() { + val constructor = BeanUtils.findPrimaryConstructor(ValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = "Hello value class!" + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ValueClass(value)) + } + + @Test + fun `Instantiate value class with multiple constructors`() { + val constructor = BeanUtils.findPrimaryConstructor(ValueClassWithMultipleConstructors::class.java)!! + assertThat(constructor).isNotNull() + val value = "Hello value class!" + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ValueClassWithMultipleConstructors(value)) + } + + @Test + fun `Instantiate class with value class parameter`() { + val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = ValueClass("Hello value class!") + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ConstructorWithValueClass(value)) + } + + @Test + fun `Instantiate class with nullable value class parameter`() { + val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithNullableValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = ValueClass("Hello value class!") + var instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ConstructorWithNullableValueClass(value)) + instance = BeanUtils.instantiateClass(constructor, null) + assertThat(instance).isEqualTo(ConstructorWithNullableValueClass(null)) + } + + @Test + fun `Instantiate primitive value class`() { + val constructor = BeanUtils.findPrimaryConstructor(PrimitiveValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = 0 + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(PrimitiveValueClass(value)) + } + + @Test + fun `Instantiate class with primitive value class parameter`() { + val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithPrimitiveValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = PrimitiveValueClass(0) + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ConstructorWithPrimitiveValueClass(value)) + } + + @Test + fun `Instantiate class with nullable primitive value class parameter`() { + val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithNullablePrimitiveValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = PrimitiveValueClass(0) + var instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ConstructorWithNullablePrimitiveValueClass(value)) + instance = BeanUtils.instantiateClass(constructor, null) + assertThat(instance).isEqualTo(ConstructorWithNullablePrimitiveValueClass(null)) + } + + class Foo(val param1: String, val param2: Int) class Bar(val param1: String, val param2: Int = 12) @@ -128,4 +196,24 @@ class BeanUtilsKotlinTests { private class PrivateClass + @JvmInline + value class ValueClass(private val value: String) + + @JvmInline + value class ValueClassWithMultipleConstructors(private val value: String) { + constructor() : this("Fail") + constructor(part1: String, part2: String) : this("Fail") + } + + data class ConstructorWithValueClass(val value: ValueClass) + + data class ConstructorWithNullableValueClass(val value: ValueClass?) + + @JvmInline + value class PrimitiveValueClass(private val value: Int) + + data class ConstructorWithPrimitiveValueClass(val value: PrimitiveValueClass) + + data class ConstructorWithNullablePrimitiveValueClass(val value: PrimitiveValueClass?) + } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt index 6ba9e5dd50ce..6bb5b3936338 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.beans.factory +import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Test @@ -32,8 +33,10 @@ class BeanFactoryExtensionsTests { @Test fun `getBean with reified type parameters`() { + val foo = Foo() + every { bf.getBeanProvider(ofType()).getObject() } returns foo bf.getBean() - verify { bf.getBean(Foo::class.java) } + verify { bf.getBeanProvider>(ofType()).getObject() } } @Test @@ -47,8 +50,10 @@ class BeanFactoryExtensionsTests { fun `getBean with reified type parameters and varargs`() { val arg1 = "arg1" val arg2 = "arg2" - bf.getBean(arg1, arg2) - verify { bf.getBean(Foo::class.java, arg1, arg2) } + val bar = Bar(arg1, arg2) + every { bf.getBeanProvider(ofType()).getObject(arg1, arg2) } returns bar + bf.getBean(arg1, arg2) + verify { bf.getBeanProvider(ofType()).getObject(arg1, arg2) } } @Test @@ -58,4 +63,7 @@ class BeanFactoryExtensionsTests { } class Foo + + @Suppress("UNUSED_PARAMETER") + class Bar(arg1: String, arg2: String) } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt new file mode 100644 index 000000000000..65c71ce2ddae --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.assertj.core.api.Assertions +import org.assertj.core.api.ThrowingConsumer +import org.junit.jupiter.api.Test +import org.springframework.aot.hint.* +import org.springframework.aot.test.generate.TestGenerationContext +import org.springframework.beans.factory.config.BeanDefinition +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.InstanceSupplier +import org.springframework.beans.factory.support.RegisteredBean +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.beans.testfixture.beans.KotlinTestBean +import org.springframework.beans.testfixture.beans.KotlinTestBeanWithOptionalParameter +import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder +import org.springframework.core.test.tools.Compiled +import org.springframework.core.test.tools.TestCompiler +import org.springframework.javapoet.MethodSpec +import org.springframework.javapoet.ParameterizedTypeName +import org.springframework.javapoet.TypeSpec +import java.util.function.BiConsumer +import java.util.function.Supplier +import javax.lang.model.element.Modifier + +/** + * Kotlin tests for [InstanceSupplierCodeGenerator]. + * + * @author Sebastien Deleuze + */ +class InstanceSupplierCodeGeneratorKotlinTests { + + private val generationContext = TestGenerationContext() + + @Test + fun generateWhenHasDefaultConstructor() { + val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java) + val beanFactory = DefaultListableBeanFactory() + compile(beanFactory, beanDefinition) { instanceSupplier, compiled -> + val bean = getBean(beanFactory, beanDefinition, instanceSupplier) + Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java) + Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)") + } + Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java)) + .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)) + } + + @Test + fun generateWhenConstructorHasOptionalParameter() { + val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java) + val beanFactory = DefaultListableBeanFactory() + compile(beanFactory, beanDefinition) { instanceSupplier, compiled -> + val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier) + Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java) + Assertions.assertThat(compiled.sourceFile) + .contains("return BeanInstanceSupplier.forConstructor();") + } + Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java)) + .satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) + } + + private fun getReflectionHints(): ReflectionHints { + return generationContext.runtimeHints.reflection() + } + + private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode)) + } + } + + private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.memberCategories).contains(category) + } + } + + private fun hasMode(mode: ExecutableMode): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.mode).isEqualTo(mode) + } + } + + @Suppress("UNCHECKED_CAST") + private fun getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition, + instanceSupplier: InstanceSupplier<*>): T { + (beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier + beanFactory.registerBeanDefinition("testBean", beanDefinition) + return beanFactory.getBean("testBean") as T + } + + private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition, + result: BiConsumer, Compiled>) { + + val freshBeanFactory = DefaultListableBeanFactory(beanFactory) + freshBeanFactory.registerBeanDefinition("testBean", beanDefinition) + val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean") + val typeBuilder = DeferredTypeBuilder() + val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder) + val generator = InstanceSupplierCodeGenerator( + generationContext, generateClass.name, + generateClass.methods, false + ) + val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor() + Assertions.assertThat(instantiationDescriptor).isNotNull() + val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor) + typeBuilder.set { type: TypeSpec.Builder -> + type.addModifiers(Modifier.PUBLIC) + type.addSuperinterface( + ParameterizedTypeName.get( + Supplier::class.java, + InstanceSupplier::class.java + ) + ) + type.addMethod( + MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC) + .returns(InstanceSupplier::class.java) + .addStatement("return \$L", generatedCode).build() + ) + } + generationContext.writeGeneratedContent() + TestCompiler.forSystem().with(generationContext).compile { + result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it) + } + } + +} diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/genericBeanTests.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/support/genericBeanTests.xml index c610f9cf1591..949d7126015f 100644 --- a/spring-beans/src/test/resources/org/springframework/beans/factory/support/genericBeanTests.xml +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/genericBeanTests.xml @@ -15,56 +15,54 @@ - - + + - - + + - + - + - - - + + + - - + - - - - - 20 - 30 - - - + + + + + 20 + 30 + + + - - - - - 20 - 30 - - - + + + + + 20 + 30 + + + - - - + + + diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java index 2ad5ece17f5e..6f1efa124def 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -268,8 +267,8 @@ public Set getCustomEnumSetMismatch() { public void setCustomEnumSetMismatch(Set customEnumSet) { this.customEnumSet = new HashSet<>(customEnumSet.size()); - for (Iterator iterator = customEnumSet.iterator(); iterator.hasNext(); ) { - this.customEnumSet.add(CustomEnum.valueOf(iterator.next())); + for (String customEnumName : customEnumSet) { + this.customEnumSet.add(CustomEnum.valueOf(customEnumName)); } } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java index 43ce2dd40ed8..1fa63057745d 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,10 @@ public interface ITestBean extends AgeHolder { void setName(String name); + default void applyName(Object name) { + setName(String.valueOf(name)); + } + ITestBean getSpouse(); void setSpouse(ITestBean spouse); diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java index 02948f7eb854..54d32af54535 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ package org.springframework.beans.testfixture.beans; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,6 +49,8 @@ public class IndexedTestBean { private SortedMap sortedMap; + private MyTestBeans myTestBeans; + public IndexedTestBean() { this(true); @@ -71,6 +75,7 @@ public void populate() { TestBean tb8 = new TestBean("name8", 0); TestBean tbX = new TestBean("nameX", 0); TestBean tbY = new TestBean("nameY", 0); + TestBean tbZ = new TestBean("nameZ", 0); this.array = new TestBean[] {tb0, tb1}; this.list = new ArrayList<>(); this.list.add(tb2); @@ -87,6 +92,7 @@ public void populate() { list.add(tbY); this.map.put("key4", list); this.map.put("key5[foo]", tb8); + this.myTestBeans = new MyTestBeans(tbZ); } @@ -146,4 +152,27 @@ public void setSortedMap(SortedMap sortedMap) { this.sortedMap = sortedMap; } + public MyTestBeans getMyTestBeans() { + return myTestBeans; + } + + public void setMyTestBeans(MyTestBeans myTestBeans) { + this.myTestBeans = myTestBeans; + } + + + public static class MyTestBeans implements Iterable { + + private final Collection testBeans; + + public MyTestBeans(TestBean... testBeans) { + this.testBeans = Arrays.asList(testBeans); + } + + @Override + public Iterator iterator() { + return this.testBeans.iterator(); + } + } + } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java index a77783a865c0..ec465876997d 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java @@ -16,6 +16,8 @@ package org.springframework.beans.testfixture.beans; +import java.util.Objects; + import org.springframework.lang.Nullable; /** @@ -47,14 +49,8 @@ public boolean equals(@Nullable Object o) { if (o == null || getClass() != o.getClass()) { return false; } - - final Pet pet = (Pet) o; - - if (name != null ? !name.equals(pet.name) : pet.name != null) { - return false; - } - - return true; + Pet pet = (Pet) o; + return Objects.equals(this.name, pet.name); } @Override diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/DummyFactory.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/DummyFactory.java index 9e3d8b6511a2..2b531ce2d552 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/DummyFactory.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/DummyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,6 +103,7 @@ public String getBeanName() { } @Override + @SuppressWarnings("deprecation") public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = (AutowireCapableBeanFactory) beanFactory; this.beanFactory.applyBeanPostProcessorsBeforeInitialization(this.testBean, this.beanName); @@ -156,6 +157,7 @@ public static boolean wasPrototypeCreated() { * @see FactoryBean#getObject() */ @Override + @SuppressWarnings("deprecation") public Object getObject() throws BeansException { if (isSingleton()) { return this.testBean; diff --git a/spring-web/src/test/java/org/springframework/http/client/NoOutputStreamingStreamingSimpleHttpRequestFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/StringFactoryBean.java similarity index 56% rename from spring-web/src/test/java/org/springframework/http/client/NoOutputStreamingStreamingSimpleHttpRequestFactoryTests.java rename to spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/StringFactoryBean.java index fba977435332..65f72324536d 100644 --- a/spring-web/src/test/java/org/springframework/http/client/NoOutputStreamingStreamingSimpleHttpRequestFactoryTests.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/StringFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,27 @@ * limitations under the License. */ -package org.springframework.http.client; +package org.springframework.beans.testfixture.beans.factory; +import org.springframework.beans.factory.FactoryBean; -public class NoOutputStreamingStreamingSimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests { +public class StringFactoryBean implements FactoryBean { @Override - protected ClientHttpRequestFactory createRequestFactory() { - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setBufferRequestBody(false); - factory.setOutputStreaming(false); - return factory; + public String getObject() { + return ""; } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/DeprecatedInjectionSamples.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/DeprecatedInjectionSamples.java new file mode 100644 index 000000000000..bb8faf0cbe00 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/DeprecatedInjectionSamples.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.annotation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +public abstract class DeprecatedInjectionSamples { + + @Deprecated + public static class DeprecatedEnvironment {} + + @Deprecated + public static class DeprecatedSample { + + @Autowired + Environment environment; + + } + + public static class DeprecatedFieldInjectionPointSample { + + @Autowired + @Deprecated + Environment environment; + + } + + public static class DeprecatedFieldInjectionTypeSample { + + @Autowired + DeprecatedEnvironment environment; + } + + public static class DeprecatedPrivateFieldInjectionTypeSample { + + @Autowired + private DeprecatedEnvironment environment; + } + + public static class DeprecatedMethodInjectionPointSample { + + @Autowired + @Deprecated + void setEnvironment(Environment environment) {} + } + + public static class DeprecatedMethodInjectionTypeSample { + + @Autowired + void setEnvironment(DeprecatedEnvironment environment) {} + } + + public static class DeprecatedPrivateMethodInjectionTypeSample { + + @Autowired + private void setEnvironment(DeprecatedEnvironment environment) {} + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomBean.java new file mode 100644 index 000000000000..8bd379c95a47 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.aot; + +/** + * A bean that uses {@link CustomPropertyValue}. + * + * @author Stephane Nicoll + */ +public class CustomBean { + + private CustomPropertyValue customPropertyValue; + + public CustomPropertyValue getCustomPropertyValue() { + return this.customPropertyValue; + } + + public void setCustomPropertyValue(CustomPropertyValue customPropertyValue) { + this.customPropertyValue = customPropertyValue; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomPropertyValue.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomPropertyValue.java new file mode 100644 index 000000000000..f73b51bc975f --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomPropertyValue.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.aot; + +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; +import org.springframework.javapoet.CodeBlock; + +/** + * A custom value with its code generator {@link Delegate} implementation. + * + * @author Stephane Nicoll + */ +public record CustomPropertyValue(String value) { + + public static class ValueCodeGeneratorDelegate implements Delegate { + @Override + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof CustomPropertyValue data) { + return CodeBlock.of("new $T($S)", CustomPropertyValue.class, data.value); + } + return null; + } + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DefaultSimpleBeanContract.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DefaultSimpleBeanContract.java new file mode 100644 index 000000000000..4893369a9d2b --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DefaultSimpleBeanContract.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.aot; + +public class DefaultSimpleBeanContract implements SimpleBeanContract { + + public SimpleBean anotherSimpleBean() { + return new SimpleBean(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanArrayFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanArrayFactoryBean.java new file mode 100644 index 000000000000..7d0d532bc281 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanArrayFactoryBean.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.aot; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.AbstractFactoryBean; + +/** + * A public {@link FactoryBean} that produces an array of objects. + * + * @author Stephane Nicoll + */ +public class SimpleBeanArrayFactoryBean extends AbstractFactoryBean { + + @Override + public Class getObjectType() { + return SimpleBean[].class; + } + + @Override + protected SimpleBean[] createInstance() throws Exception { + return new SimpleBean[0]; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanContract.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanContract.java new file mode 100644 index 000000000000..96d724987407 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanContract.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.aot; + +/** + * Showcase a factory method that is defined on an interface. + * + * @author Stephane Nicoll + */ +public interface SimpleBeanContract { + + default SimpleBean simpleBean() { + return new SimpleBean(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedBean.java new file mode 100644 index 000000000000..a8d90235fbf8 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.generator.deprecation; + +/** + * A sample bean that's fully deprecated. + * + * @author Stephane Nicoll + */ +@Deprecated +public class DeprecatedBean { + + // This isn't flag deprecated on purpose + public static class Nested {} + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedConstructor.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedConstructor.java new file mode 100644 index 000000000000..bd2489ec61b9 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedConstructor.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.generator.deprecation; + +import org.springframework.core.env.Environment; + +/** + * A sample bean whose factory method (constructor) is deprecated. + * + * @author Stephane Nicoll + */ +public class DeprecatedConstructor { + + @Deprecated + public DeprecatedConstructor(Environment environment) { + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalBean.java new file mode 100644 index 000000000000..0d1e6b9bfae0 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalBean.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.generator.deprecation; + +/** + * A sample bean that's fully deprecated for removal. + * + * @author Stephane Nicoll + */ +@Deprecated(forRemoval = true) +public class DeprecatedForRemovalBean { + + // This isn't flag deprecated on purpose + public static class Nested {} +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalConstructor.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalConstructor.java new file mode 100644 index 000000000000..1e3599c8dad9 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalConstructor.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.generator.deprecation; + +import org.springframework.core.env.Environment; + +/** + * A sample bean whose factory method (constructor) is deprecated for removal. + * + * @author Stephane Nicoll + */ +public class DeprecatedForRemovalConstructor { + + @Deprecated(forRemoval = true) + public DeprecatedForRemovalConstructor(Environment environment) { + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java new file mode 100644 index 000000000000..be9246092bfb --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.generator.deprecation; + +/** + * A class with deprecated members for removal to test various use cases. + * + * @author Stephane Nicoll + */ +public class DeprecatedForRemovalMemberConfiguration { + + @Deprecated(forRemoval = true) + public String deprecatedString() { + return "deprecated"; + } + + @SuppressWarnings("removal") + public String deprecatedParameter(DeprecatedForRemovalBean bean) { + return bean.toString(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java new file mode 100644 index 000000000000..30ab30ef7c73 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.generator.deprecation; + +/** + * A class with deprecated members to test various use cases. + * + * @author Stephane Nicoll + */ +public class DeprecatedMemberConfiguration { + + @Deprecated + public String deprecatedString() { + return "deprecated"; + } + + @SuppressWarnings("deprecation") + public String deprecatedParameter(DeprecatedBean bean) { + return bean.toString(); + } + + @SuppressWarnings("deprecation") + public DeprecatedBean deprecatedReturnType() { + return new DeprecatedBean(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java index ff762788faae..6039e7d540ab 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.beans.testfixture.factory.xml; -import java.beans.PropertyEditorSupport; -import java.util.StringTokenizer; - import org.junit.jupiter.api.Test; import org.springframework.beans.BeansException; @@ -53,7 +50,7 @@ public abstract class AbstractBeanFactoryTests { * Roderick bean inherits from rod, overriding name only. */ @Test - public void inheritance() { + protected void inheritance() { assertThat(getBeanFactory().containsBean("rod")).isTrue(); assertThat(getBeanFactory().containsBean("roderick")).isTrue(); TestBean rod = (TestBean) getBeanFactory().getBean("rod"); @@ -66,7 +63,7 @@ public void inheritance() { } @Test - public void getBeanWithNullArg() { + protected void getBeanWithNullArg() { assertThatIllegalArgumentException().isThrownBy(() -> getBeanFactory().getBean((String) null)); } @@ -75,7 +72,7 @@ public void getBeanWithNullArg() { * Test that InitializingBean objects receive the afterPropertiesSet() callback */ @Test - public void initializingBeanCallback() { + protected void initializingBeanCallback() { MustBeInitialized mbi = (MustBeInitialized) getBeanFactory().getBean("mustBeInitialized"); // The dummy business method will throw an exception if the // afterPropertiesSet() callback wasn't invoked @@ -87,7 +84,7 @@ public void initializingBeanCallback() { * afterPropertiesSet() callback before BeanFactoryAware callbacks */ @Test - public void lifecycleCallbacks() { + protected void lifecycleCallbacks() { LifecycleBean lb = (LifecycleBean) getBeanFactory().getBean("lifecycle"); assertThat(lb.getBeanName()).isEqualTo("lifecycle"); // The dummy business method will throw an exception if the @@ -98,24 +95,22 @@ public void lifecycleCallbacks() { } @Test - public void findsValidInstance() { + protected void findsValidInstance() { Object o = getBeanFactory().getBean("rod"); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); - TestBean rod = (TestBean) o; - assertThat(rod.getName().equals("Rod")).as("rod.name is Rod").isTrue(); - assertThat(rod.getAge()).as("rod.age is 31").isEqualTo(31); + assertThat(o).isInstanceOfSatisfying(TestBean.class, rod -> { + assertThat(rod.getName().equals("Rod")).as("rod.name is Rod").isTrue(); + assertThat(rod.getAge()).as("rod.age is 31").isEqualTo(31); + }); } @Test - public void getInstanceByMatchingClass() { + protected void getInstanceByMatchingClass() { Object o = getBeanFactory().getBean("rod", TestBean.class); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); } @Test - public void getInstanceByNonmatchingClass() { + protected void getInstanceByNonmatchingClass() { assertThatExceptionOfType(BeanNotOfRequiredTypeException.class).isThrownBy(() -> getBeanFactory().getBean("rod", BeanFactory.class)) .satisfies(ex -> { @@ -126,21 +121,19 @@ public void getInstanceByNonmatchingClass() { } @Test - public void getSharedInstanceByMatchingClass() { + protected void getSharedInstanceByMatchingClass() { Object o = getBeanFactory().getBean("rod", TestBean.class); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); } @Test - public void getSharedInstanceByMatchingClassNoCatch() { + protected void getSharedInstanceByMatchingClassNoCatch() { Object o = getBeanFactory().getBean("rod", TestBean.class); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); } @Test - public void getSharedInstanceByNonmatchingClass() { + protected void getSharedInstanceByNonmatchingClass() { assertThatExceptionOfType(BeanNotOfRequiredTypeException.class).isThrownBy(() -> getBeanFactory().getBean("rod", BeanFactory.class)) .satisfies(ex -> { @@ -151,18 +144,15 @@ public void getSharedInstanceByNonmatchingClass() { } @Test - public void sharedInstancesAreEqual() { + protected void sharedInstancesAreEqual() { Object o = getBeanFactory().getBean("rod"); - boolean condition1 = o instanceof TestBean; - assertThat(condition1).as("Rod bean1 is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); Object o1 = getBeanFactory().getBean("rod"); - boolean condition = o1 instanceof TestBean; - assertThat(condition).as("Rod bean2 is a TestBean").isTrue(); - assertThat(o).as("Object equals applies").isSameAs(o1); + assertThat(o1).isInstanceOf(TestBean.class).isSameAs(o); } @Test - public void prototypeInstancesAreIndependent() { + protected void prototypeInstancesAreIndependent() { TestBean tb1 = (TestBean) getBeanFactory().getBean("kathy"); TestBean tb2 = (TestBean) getBeanFactory().getBean("kathy"); assertThat(tb1).as("ref equal DOES NOT apply").isNotSameAs(tb2); @@ -176,36 +166,37 @@ public void prototypeInstancesAreIndependent() { } @Test - public void notThere() { + protected void notThere() { assertThat(getBeanFactory().containsBean("Mr Squiggle")).isFalse(); assertThatExceptionOfType(BeansException.class).isThrownBy(() -> getBeanFactory().getBean("Mr Squiggle")); } @Test - public void validEmpty() { + protected void validEmpty() { Object o = getBeanFactory().getBean("validEmpty"); - boolean condition = o instanceof TestBean; - assertThat(condition).as("validEmpty bean is a TestBean").isTrue(); - TestBean ve = (TestBean) o; - assertThat(ve.getName() == null && ve.getAge() == 0 && ve.getSpouse() == null).as("Valid empty has defaults").isTrue(); + assertThat(o).isInstanceOfSatisfying(TestBean.class, ve -> { + assertThat(ve.getName()).isNull(); + assertThat(ve.getAge()).isEqualTo(0); + assertThat(ve.getSpouse()).isNull(); + }); } @Test - public void typeMismatch() { + protected void typeMismatch() { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> getBeanFactory().getBean("typeMismatch")) .withCauseInstanceOf(TypeMismatchException.class); } @Test - public void grandparentDefinitionFoundInBeanFactory() throws Exception { + protected void grandparentDefinitionFoundInBeanFactory() { TestBean dad = (TestBean) getBeanFactory().getBean("father"); assertThat(dad.getName().equals("Albert")).as("Dad has correct name").isTrue(); } @Test - public void factorySingleton() throws Exception { + protected void factorySingleton() { assertThat(getBeanFactory().isSingleton("&singletonFactory")).isTrue(); assertThat(getBeanFactory().isSingleton("singletonFactory")).isTrue(); TestBean tb = (TestBean) getBeanFactory().getBean("singletonFactory"); @@ -217,12 +208,11 @@ public void factorySingleton() throws Exception { } @Test - public void factoryPrototype() throws Exception { + protected void factoryPrototype() { assertThat(getBeanFactory().isSingleton("&prototypeFactory")).isTrue(); assertThat(getBeanFactory().isSingleton("prototypeFactory")).isFalse(); TestBean tb = (TestBean) getBeanFactory().getBean("prototypeFactory"); - boolean condition = !tb.getName().equals(DummyFactory.SINGLETON_NAME); - assertThat(condition).isTrue(); + assertThat(tb.getName()).isNotEqualTo(DummyFactory.SINGLETON_NAME); TestBean tb2 = (TestBean) getBeanFactory().getBean("prototypeFactory"); assertThat(tb).as("Prototype references !=").isNotSameAs(tb2); } @@ -232,7 +222,7 @@ public void factoryPrototype() throws Exception { * This is only possible if we're dealing with a factory */ @Test - public void getFactoryItself() throws Exception { + protected void getFactoryItself() { assertThat(getBeanFactory().getBean("&singletonFactory")).isNotNull(); } @@ -240,7 +230,7 @@ public void getFactoryItself() throws Exception { * Check that afterPropertiesSet gets called on factory */ @Test - public void factoryIsInitialized() throws Exception { + protected void factoryIsInitialized() { TestBean tb = (TestBean) getBeanFactory().getBean("singletonFactory"); assertThat(tb).isNotNull(); DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); @@ -251,7 +241,7 @@ public void factoryIsInitialized() throws Exception { * It should be illegal to dereference a normal bean as a factory. */ @Test - public void rejectsFactoryGetOnNormalBean() { + protected void rejectsFactoryGetOnNormalBean() { assertThatExceptionOfType(BeanIsNotAFactoryException.class).isThrownBy(() -> getBeanFactory().getBean("&rod")); } @@ -259,7 +249,7 @@ public void rejectsFactoryGetOnNormalBean() { // TODO: refactor in AbstractBeanFactory (tests for AbstractBeanFactory) // and rename this class @Test - public void aliasing() { + protected void aliasing() { BeanFactory bf = getBeanFactory(); if (!(bf instanceof ConfigurableBeanFactory cbf)) { return; @@ -277,17 +267,4 @@ public void aliasing() { assertThat(rod).isSameAs(aliasRod); } - - public static class TestBeanEditor extends PropertyEditorSupport { - - @Override - public void setAsText(String text) { - TestBean tb = new TestBean(); - StringTokenizer st = new StringTokenizer(text, "_"); - tb.setName(st.nextToken()); - tb.setAge(Integer.parseInt(st.nextToken())); - setValue(tb); - } - } - } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java index c9644f5a6c63..21fa185919ee 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ protected ListableBeanFactory getListableBeanFactory() { * Subclasses can override this. */ @Test - public void count() { + protected void count() { assertCount(13); } @@ -66,7 +66,7 @@ protected void assertTestBeanCount(int count) { } @Test - public void getDefinitionsForNoSuchClass() { + protected void getDefinitionsForNoSuchClass() { String[] defnames = getListableBeanFactory().getBeanNamesForType(String.class); assertThat(defnames.length).as("No string definitions").isEqualTo(0); } @@ -76,7 +76,7 @@ public void getDefinitionsForNoSuchClass() { * what type factories may return, and it may even change over time.) */ @Test - public void getCountForFactoryClass() { + protected void getCountForFactoryClass() { assertThat(getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length).as("Should have 2 factories, not " + getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length).isEqualTo(2); @@ -85,7 +85,7 @@ public void getCountForFactoryClass() { } @Test - public void containsBeanDefinition() { + protected void containsBeanDefinition() { assertThat(getListableBeanFactory().containsBeanDefinition("rod")).isTrue(); assertThat(getListableBeanFactory().containsBeanDefinition("roderick")).isTrue(); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/RecordBean.java b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBean.kt similarity index 86% rename from spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/RecordBean.java rename to spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBean.kt index 75419ab1cb73..1cdcc078b3db 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/RecordBean.java +++ b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBean.kt @@ -14,6 +14,6 @@ * limitations under the License. */ -package org.springframework.beans.testfixture.beans; +package org.springframework.beans.testfixture.beans -public record RecordBean(String name) { } +class KotlinTestBean diff --git a/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBeanWithOptionalParameter.kt b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBeanWithOptionalParameter.kt new file mode 100644 index 000000000000..60ba9999be56 --- /dev/null +++ b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBeanWithOptionalParameter.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans + +class KotlinTestBeanWithOptionalParameter(private val other: KotlinTestBean = KotlinTestBean()) diff --git a/spring-beans/src/testFixtures/resources/META-INF/spring/aot.factories b/spring-beans/src/testFixtures/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000000..5f3eadf96ddf --- /dev/null +++ b/spring-beans/src/testFixtures/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.generate.ValueCodeGenerator$Delegate=\ +org.springframework.beans.testfixture.beans.factory.aot.CustomPropertyValue$ValueCodeGeneratorDelegate \ No newline at end of file diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java index d9a1fa5af607..87aa1f506d82 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java @@ -42,7 +42,9 @@ * @author Stephane Nicoll * @author Juergen Hoeller * @since 5.0 + * @deprecated as of 6.1, in favor of the AOT engine. */ +@Deprecated(since = "6.1", forRemoval = true) public class CandidateComponentsIndexer implements Processor { private MetadataStore metadataStore; diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java index 6b36136ba6a0..39b8d987ce62 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,8 @@ /** * A {@link StereotypesProvider} that extracts a stereotype for each - * {@code jakarta.*} annotation present on a class or interface. + * {@code jakarta.*} or {@code javax.*} annotation present on a class or + * interface. * * @author Stephane Nicoll * @since 5.0 @@ -49,7 +50,7 @@ public Set getStereotypes(Element element) { } for (AnnotationMirror annotation : this.typeHelper.getAllAnnotationMirrors(element)) { String type = this.typeHelper.getType(annotation); - if (type.startsWith("jakarta.")) { + if (type.startsWith("jakarta.") || type.startsWith("javax.")) { stereotypes.add(type); } } diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java index ca779c4ed657..995081488379 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -199,7 +199,7 @@ void typeStereotypeOnIndexedInterface() { @Test void embeddedCandidatesAreDetected() - throws IOException, ClassNotFoundException { + throws ClassNotFoundException { // Validate nested type structure String nestedType = "org.springframework.context.index.sample.SampleEmbedded.Another$AnotherPublicCandidate"; Class type = ClassUtils.forName(nestedType, getClass().getClassLoader()); @@ -231,12 +231,14 @@ private void testSingleComponent(Class target, Class... stereotypes) { assertThat(metadata.getItems()).hasSize(1); } + @SuppressWarnings("removal") private CandidateComponentsMetadata compile(Class... types) { CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); this.compiler.getTask(types).call(processor); return readGeneratedMetadata(this.compiler.getOutputLocation()); } + @SuppressWarnings("removal") private CandidateComponentsMetadata compile(String... types) { CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); this.compiler.getTask(types).call(processor); @@ -247,8 +249,7 @@ private CandidateComponentsMetadata readGeneratedMetadata(File outputLocation) { File metadataFile = new File(outputLocation, MetadataStore.METADATA_PATH); if (metadataFile.isFile()) { try (FileInputStream fileInputStream = new FileInputStream(metadataFile)) { - CandidateComponentsMetadata metadata = PropertiesMarshaller.read(fileInputStream); - return metadata; + return PropertiesMarshaller.read(fileInputStream); } catch (IOException ex) { throw new IllegalStateException("Failed to read metadata from disk", ex); diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java index e518841ef3a5..fcda73406e8d 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,10 @@ * @author Stephane Nicoll * @author Vedran Pavic */ -public class PropertiesMarshallerTests { +class PropertiesMarshallerTests { @Test - public void readWrite() throws IOException { + void readWrite() throws IOException { CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); metadata.add(createItem("com.foo", "first", "second")); metadata.add(createItem("com.bar", "first")); @@ -51,7 +51,7 @@ public void readWrite() throws IOException { } @Test - public void metadataIsWrittenDeterministically() throws IOException { + void metadataIsWrittenDeterministically() throws IOException { CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); metadata.add(createItem("com.b", "type")); metadata.add(createItem("com.c", "type")); @@ -59,7 +59,7 @@ public void metadataIsWrittenDeterministically() throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PropertiesMarshaller.write(metadata, outputStream); - String contents = new String(outputStream.toByteArray(), StandardCharsets.ISO_8859_1); + String contents = outputStream.toString(StandardCharsets.ISO_8859_1); assertThat(contents.split(System.lineSeparator())).containsExactly("com.a=type", "com.b=type", "com.c=type"); } diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java index b96de6139374..f128d697c984 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ /** * Copy of the {@code @Scope} annotation for testing purposes. */ -@Target({ ElementType.TYPE, ElementType.METHOD }) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Scope { diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle index 715b922b6809..a2f0c083e734 100644 --- a/spring-context-support/spring-context-support.gradle +++ b/spring-context-support/spring-context-support.gradle @@ -6,12 +6,12 @@ dependencies { api(project(":spring-core")) optional(project(":spring-jdbc")) // for Quartz support optional(project(":spring-tx")) // for Quartz support + optional("com.github.ben-manes.caffeine:caffeine") optional("jakarta.activation:jakarta.activation-api") optional("jakarta.mail:jakarta.mail-api") optional("javax.cache:cache-api") - optional("com.github.ben-manes.caffeine:caffeine") - optional("org.quartz-scheduler:quartz") optional("org.freemarker:freemarker") + optional("org.quartz-scheduler:quartz") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation("org.assertj:assertj-core") testFixturesImplementation("org.mockito:mockito-core") @@ -20,10 +20,11 @@ dependencies { testImplementation(testFixtures(project(":spring-context"))) testImplementation(testFixtures(project(":spring-core"))) testImplementation(testFixtures(project(":spring-tx"))) - testImplementation("org.hsqldb:hsqldb") + testImplementation("io.projectreactor:reactor-core") testImplementation("jakarta.annotation:jakarta.annotation-api") - testRuntimeOnly("org.ehcache:jcache") + testImplementation("org.hsqldb:hsqldb") + testRuntimeOnly("com.sun.mail:jakarta.mail") testRuntimeOnly("org.ehcache:ehcache") + testRuntimeOnly("org.ehcache:jcache") testRuntimeOnly("org.glassfish:jakarta.el") - testRuntimeOnly("com.sun.mail:jakarta.mail") } diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java index c768347cf204..da4b62e471c6 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java @@ -17,8 +17,11 @@ package org.springframework.cache.caffeine; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import java.util.function.Supplier; +import com.github.benmanes.caffeine.cache.AsyncCache; import com.github.benmanes.caffeine.cache.LoadingCache; import org.springframework.cache.support.AbstractValueAdaptingCache; @@ -29,7 +32,11 @@ * Spring {@link org.springframework.cache.Cache} adapter implementation * on top of a Caffeine {@link com.github.benmanes.caffeine.cache.Cache} instance. * - *

Requires Caffeine 2.1 or higher. + *

Supports the {@link #retrieve(Object)} and {@link #retrieve(Object, Supplier)} + * operations through Caffeine's {@link AsyncCache}, when provided via the + * {@link #CaffeineCache(String, AsyncCache, boolean)} constructor. + * + *

Requires Caffeine 3.0 or higher, as of Spring Framework 6.1. * * @author Ben Manes * @author Juergen Hoeller @@ -43,6 +50,9 @@ public class CaffeineCache extends AbstractValueAdaptingCache { private final com.github.benmanes.caffeine.cache.Cache cache; + @Nullable + private AsyncCache asyncCache; + /** * Create a {@link CaffeineCache} instance with the specified name and the @@ -72,17 +82,52 @@ public CaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache, boolean allowNullValues) { + super(allowNullValues); + Assert.notNull(name, "Name must not be null"); + Assert.notNull(cache, "Cache must not be null"); + this.name = name; + this.cache = cache.synchronous(); + this.asyncCache = cache; + } + @Override public final String getName() { return this.name; } + /** + * Return the internal Caffeine Cache + * (possibly an adapter on top of an {@link #getAsyncCache()}). + */ @Override public final com.github.benmanes.caffeine.cache.Cache getNativeCache() { return this.cache; } + /** + * Return the internal Caffeine AsyncCache. + * @throws IllegalStateException if no AsyncCache is available + * @since 6.1 + * @see #CaffeineCache(String, AsyncCache, boolean) + * @see CaffeineCacheManager#setAsyncCacheMode + */ + public final AsyncCache getAsyncCache() { + Assert.state(this.asyncCache != null, + "No Caffeine AsyncCache available: set CaffeineCacheManager.setAsyncCacheMode(true)"); + return this.asyncCache; + } + @SuppressWarnings("unchecked") @Override @Nullable @@ -90,6 +135,29 @@ public T get(Object key, Callable valueLoader) { return (T) fromStoreValue(this.cache.get(key, new LoadFunction(valueLoader))); } + @Override + @Nullable + public CompletableFuture retrieve(Object key) { + CompletableFuture result = getAsyncCache().getIfPresent(key); + if (result != null && isAllowNullValues()) { + result = result.thenApply(this::toValueWrapper); + } + return result; + } + + @SuppressWarnings("unchecked") + @Override + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + if (isAllowNullValues()) { + return (CompletableFuture) getAsyncCache() + .get(key, (k, e) -> valueLoader.get().thenApply(this::toStoreValue)) + .thenApply(this::fromStoreValue); + } + else { + return (CompletableFuture) getAsyncCache().get(key, (k, e) -> valueLoader.get()); + } + } + @Override @Nullable protected Object lookup(Object key) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index e80cb747f396..2518f3a57aa9 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -22,7 +22,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.AsyncCacheLoader; import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.CaffeineSpec; @@ -45,7 +48,12 @@ * A {@link CaffeineSpec}-compliant expression value can also be applied * via the {@link #setCacheSpecification "cacheSpecification"} bean property. * - *

Requires Caffeine 2.1 or higher. + *

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

Requires Caffeine 3.0 or higher, as of Spring Framework 6.1. * * @author Ben Manes * @author Juergen Hoeller @@ -54,13 +62,18 @@ * @author Brian Clozel * @since 4.3 * @see CaffeineCache + * @see #setCaffeineSpec + * @see #setCacheSpecification + * @see #setAsyncCacheMode */ public class CaffeineCacheManager implements CacheManager { private Caffeine cacheBuilder = Caffeine.newBuilder(); @Nullable - private CacheLoader cacheLoader; + private AsyncCacheLoader cacheLoader; + + private boolean asyncCacheMode = false; private boolean allowNullValues = true; @@ -159,6 +172,57 @@ public void setCacheLoader(CacheLoader cacheLoader) { } } + /** + * Set the Caffeine AsyncCacheLoader to use for building each individual + * {@link CaffeineCache} instance, turning it into a LoadingCache. + *

This implicitly switches the {@link #setAsyncCacheMode "asyncCacheMode"} + * flag to {@code true}. + * @since 6.1 + * @see #createAsyncCaffeineCache + * @see Caffeine#buildAsync(AsyncCacheLoader) + * @see com.github.benmanes.caffeine.cache.LoadingCache + */ + public void setAsyncCacheLoader(AsyncCacheLoader cacheLoader) { + if (!ObjectUtils.nullSafeEquals(this.cacheLoader, cacheLoader)) { + this.cacheLoader = cacheLoader; + this.asyncCacheMode = true; + refreshCommonCaches(); + } + } + + /** + * Set the common cache type that this cache manager builds to async. + * This applies to {@link #setCacheNames} as well as on-demand caches. + *

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

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

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

If you come here for the adaptation of reactive types such as a Reactor + * {@code Mono} or {@code Flux} onto asynchronous caching, we recommend the standard + * arrangement for caching the produced values asynchronously in 6.1 through enabling + * this Caffeine mode. If this is not immediately possible/desirable for existing + * apps, you may set the system property "spring.cache.reactivestreams.ignore=true" + * to restore 6.0 behavior where reactive handles are treated as regular values. + * @since 6.1 + * @see Caffeine#buildAsync() + * @see Cache#retrieve(Object) + * @see Cache#retrieve(Object, Supplier) + * @see org.springframework.cache.interceptor.CacheAspectSupport#IGNORE_REACTIVESTREAMS_PROPERTY_NAME + */ + public void setAsyncCacheMode(boolean asyncCacheMode) { + if (this.asyncCacheMode != asyncCacheMode) { + this.asyncCacheMode = asyncCacheMode; + refreshCommonCaches(); + } + } + /** * Specify whether to accept and convert {@code null} values for all caches * in this cache manager. @@ -211,13 +275,34 @@ public Cache getCache(String name) { * @param name the name of the cache * @param cache the custom Caffeine Cache instance to register * @since 5.2.8 - * @see #adaptCaffeineCache + * @see #adaptCaffeineCache(String, com.github.benmanes.caffeine.cache.Cache) */ public void registerCustomCache(String name, com.github.benmanes.caffeine.cache.Cache cache) { this.customCacheNames.add(name); this.cacheMap.put(name, adaptCaffeineCache(name, cache)); } + /** + * Register the given Caffeine AsyncCache instance with this cache manager, + * adapting it to Spring's cache API for exposure through {@link #getCache}. + * Any number of such custom caches may be registered side by side. + *

This allows for custom settings per cache (as opposed to all caches + * sharing the common settings in the cache manager's configuration) and + * is typically used with the Caffeine builder API: + * {@code registerCustomCache("myCache", Caffeine.newBuilder().maximumSize(10).buildAsync())} + *

Note that any other caches, whether statically specified through + * {@link #setCacheNames} or dynamically built on demand, still operate + * with the common settings in the cache manager's configuration. + * @param name the name of the cache + * @param cache the custom Caffeine AsyncCache instance to register + * @since 6.1 + * @see #adaptCaffeineCache(String, AsyncCache) + */ + public void registerCustomCache(String name, AsyncCache cache) { + this.customCacheNames.add(name); + this.cacheMap.put(name, adaptCaffeineCache(name, cache)); + } + /** * Adapt the given new native Caffeine Cache instance to Spring's {@link Cache} * abstraction for the specified cache name. @@ -225,18 +310,32 @@ public void registerCustomCache(String name, com.github.benmanes.caffeine.cache. * @param cache the native Caffeine Cache instance * @return the Spring CaffeineCache adapter (or a decorator thereof) * @since 5.2.8 - * @see CaffeineCache + * @see CaffeineCache#CaffeineCache(String, com.github.benmanes.caffeine.cache.Cache, boolean) * @see #isAllowNullValues() */ protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache) { return new CaffeineCache(name, cache, isAllowNullValues()); } + /** + * Adapt the given new Caffeine AsyncCache instance to Spring's {@link Cache} + * abstraction for the specified cache name. + * @param name the name of the cache + * @param cache the Caffeine AsyncCache instance + * @return the Spring CaffeineCache adapter (or a decorator thereof) + * @since 6.1 + * @see CaffeineCache#CaffeineCache(String, AsyncCache, boolean) + * @see #isAllowNullValues() + */ + protected Cache adaptCaffeineCache(String name, AsyncCache cache) { + return new CaffeineCache(name, cache, isAllowNullValues()); + } + /** * Build a common {@link CaffeineCache} instance for the specified cache name, * using the common Caffeine configuration specified on this cache manager. *

Delegates to {@link #adaptCaffeineCache} as the adaptation method to - * Spring's cache abstraction (allowing for centralized decoration etc), + * Spring's cache abstraction (allowing for centralized decoration etc.), * passing in a freshly built native Caffeine Cache instance. * @param name the name of the cache * @return the Spring CaffeineCache adapter (or a decorator thereof) @@ -244,7 +343,8 @@ protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cac * @see #createNativeCaffeineCache */ protected Cache createCaffeineCache(String name) { - return adaptCaffeineCache(name, createNativeCaffeineCache(name)); + return (this.asyncCacheMode ? adaptCaffeineCache(name, createAsyncCaffeineCache(name)) : + adaptCaffeineCache(name, createNativeCaffeineCache(name))); } /** @@ -255,7 +355,29 @@ protected Cache createCaffeineCache(String name) { * @see #createCaffeineCache */ protected com.github.benmanes.caffeine.cache.Cache createNativeCaffeineCache(String name) { - return (this.cacheLoader != null ? this.cacheBuilder.build(this.cacheLoader) : this.cacheBuilder.build()); + if (this.cacheLoader != null) { + if (this.cacheLoader instanceof CacheLoader regularCacheLoader) { + return this.cacheBuilder.build(regularCacheLoader); + } + else { + throw new IllegalStateException( + "Cannot create regular Caffeine Cache with async-only cache loader: " + this.cacheLoader); + } + } + return this.cacheBuilder.build(); + } + + /** + * Build a common Caffeine AsyncCache instance for the specified cache name, + * using the common Caffeine configuration specified on this cache manager. + * @param name the name of the cache + * @return the Caffeine AsyncCache instance + * @since 6.1 + * @see #createCaffeineCache + */ + protected AsyncCache createAsyncCaffeineCache(String name) { + return (this.cacheLoader != null ? this.cacheBuilder.buildAsync(this.cacheLoader) : + this.cacheBuilder.buildAsync()); } /** diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java index 84d2e3f9bcf2..c870d843d736 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.cache.jcache; import java.util.concurrent.Callable; +import java.util.function.Function; import javax.cache.Cache; import javax.cache.processor.EntryProcessor; @@ -42,6 +43,8 @@ public class JCacheCache extends AbstractValueAdaptingCache { private final Cache cache; + private final ValueLoaderEntryProcessor valueLoaderEntryProcessor; + /** * Create a {@code JCacheCache} instance. @@ -60,6 +63,8 @@ public JCacheCache(Cache jcache, boolean allowNullValues) { super(allowNullValues); Assert.notNull(jcache, "Cache must not be null"); this.cache = jcache; + this.valueLoaderEntryProcessor = new ValueLoaderEntryProcessor( + this::fromStoreValue, this::toStoreValue); } @@ -81,9 +86,10 @@ protected Object lookup(Object key) { @Override @Nullable + @SuppressWarnings("unchecked") public T get(Object key, Callable valueLoader) { try { - return this.cache.invoke(key, new ValueLoaderEntryProcessor(), valueLoader); + return (T) this.cache.invoke(key, this.valueLoaderEntryProcessor, valueLoader); } catch (EntryProcessorException ex) { throw new ValueRetrievalException(key, valueLoader, ex.getCause()); @@ -98,8 +104,8 @@ public void put(Object key, @Nullable Object value) { @Override @Nullable public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { - boolean set = this.cache.putIfAbsent(key, toStoreValue(value)); - return (set ? null : get(key)); + Object previous = this.cache.invoke(key, PutIfAbsentEntryProcessor.INSTANCE, toStoreValue(value)); + return (previous != null ? toValueWrapper(previous) : null); } @Override @@ -125,18 +131,45 @@ public boolean invalidate() { } - private class ValueLoaderEntryProcessor implements EntryProcessor { + private static class PutIfAbsentEntryProcessor implements EntryProcessor { + + private static final PutIfAbsentEntryProcessor INSTANCE = new PutIfAbsentEntryProcessor(); - @SuppressWarnings("unchecked") @Override @Nullable - public T process(MutableEntry entry, Object... arguments) throws EntryProcessorException { - Callable valueLoader = (Callable) arguments[0]; + public Object process(MutableEntry entry, Object... arguments) throws EntryProcessorException { + Object existingValue = entry.getValue(); + if (existingValue == null) { + entry.setValue(arguments[0]); + } + return existingValue; + } + } + + + private static final class ValueLoaderEntryProcessor implements EntryProcessor { + + private final Function fromStoreValue; + + private final Function toStoreValue; + + private ValueLoaderEntryProcessor(Function fromStoreValue, + Function toStoreValue) { + + this.fromStoreValue = fromStoreValue; + this.toStoreValue = toStoreValue; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public Object process(MutableEntry entry, Object... arguments) throws EntryProcessorException { + Callable valueLoader = (Callable) arguments[0]; if (entry.exists()) { - return (T) fromStoreValue(entry.getValue()); + return this.fromStoreValue.apply(entry.getValue()); } else { - T value; + Object value; try { value = valueLoader.call(); } @@ -144,7 +177,7 @@ public T process(MutableEntry entry, Object... arguments) throws throw new EntryProcessorException("Value loader '" + valueLoader + "' failed " + "to compute value for key '" + entry.getKey() + "'", ex); } - entry.setValue(toStoreValue(value)); + entry.setValue(this.toStoreValue.apply(value)); return value; } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java index e4feb09554ba..0e87c8af53ad 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java @@ -121,6 +121,7 @@ protected Collection loadCaches() { } @Override + @Nullable protected Cache getMissingCache(String name) { CacheManager cacheManager = getCacheManager(); Assert.state(cacheManager != null, "No CacheManager set"); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java index 8b20e4b14822..d59bd01a49a4 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,12 +29,10 @@ import org.springframework.lang.Nullable; /** - * Abstract implementation of {@link JCacheOperationSource} that caches attributes + * Abstract implementation of {@link JCacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. declaring method. * - *

This implementation caches attributes by method after they are first used. - * * @author Stephane Nicoll * @author Juergen Hoeller * @since 4.1 @@ -43,24 +41,25 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Object NULL_CACHING_ATTRIBUTE = new Object(); + private static final Object NULL_CACHING_MARKER = new Object(); protected final Log logger = LogFactory.getLog(getClass()); - private final Map cache = new ConcurrentHashMap<>(1024); + private final Map operationCache = new ConcurrentHashMap<>(1024); @Override + @Nullable public JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { MethodClassKey cacheKey = new MethodClassKey(method, targetClass); - Object cached = this.cache.get(cacheKey); + Object cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? (JCacheOperation) cached : null); + return (cached != NULL_CACHING_MARKER ? (JCacheOperation) cached : null); } else { JCacheOperation operation = computeCacheOperation(method, targetClass); @@ -68,10 +67,10 @@ public JCacheOperation getCacheOperation(Method method, @Nullable Class ta if (logger.isDebugEnabled()) { logger.debug("Adding cacheable method '" + method.getName() + "' with operation: " + operation); } - this.cache.put(cacheKey, operation); + this.operationCache.put(cacheKey, operation); } else { - this.cache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return operation; } @@ -84,7 +83,7 @@ private JCacheOperation computeCacheOperation(Method method, @Nullable Class< return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java index b289b14715f4..82db7a9a6ac9 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ public abstract class AnnotationJCacheOperationSource extends AbstractFallbackJCacheOperationSource { @Override + @Nullable protected JCacheOperation findCacheOperation(Method method, @Nullable Class targetType) { CacheResult cacheResult = method.getAnnotation(CacheResult.class); CachePut cachePut = method.getAnnotation(CachePut.class); @@ -212,10 +213,8 @@ protected String generateDefaultCacheName(Method method) { for (Class parameterType : parameterTypes) { parameters.add(parameterType.getName()); } - - return method.getDeclaringClass().getName() - + '.' + method.getName() - + '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')'; + return method.getDeclaringClass().getName() + '.' + method.getName() + + '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')'; } private int countNonNull(Object... instances) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java index 51fda366b04b..54e4dcaefb6b 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactory * Set the cache operation attribute source which is used to find cache * attributes. This should usually be identical to the source reference * set on the cache interceptor itself. + * @see JCacheInterceptor#setCacheOperationSource */ public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { this.pointcut.setCacheOperationSource(cacheOperationSource); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java index d71af3244dba..f64c09a336f1 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java @@ -23,6 +23,7 @@ import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.lang.Nullable; /** * Intercept methods annotated with {@link CachePut}. @@ -39,6 +40,7 @@ public CachePutInterceptor(CacheErrorHandler errorHandler) { @Override + @Nullable protected Object invoke( CacheOperationInvocationContext context, CacheOperationInvoker invoker) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java index efbfb65652e0..a406418863f2 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,7 +102,7 @@ private static CacheParameterDetail initializeValueParameterDetail( result = parameter; } else { - throw new IllegalArgumentException("More than one @CacheValue found on " + method + ""); + throw new IllegalArgumentException("More than one @CacheValue found on " + method); } } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java index 66b583e37870..dfd8f0a4cc0e 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java @@ -22,6 +22,7 @@ import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.lang.Nullable; /** * Intercept methods annotated with {@link CacheRemoveAll}. @@ -38,6 +39,7 @@ protected CacheRemoveAllInterceptor(CacheErrorHandler errorHandler) { @Override + @Nullable protected Object invoke( CacheOperationInvocationContext context, CacheOperationInvoker invoker) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java index 84852783166a..a1075785bb24 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java @@ -22,6 +22,7 @@ import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.lang.Nullable; /** * Intercept methods annotated with {@link CacheRemove}. @@ -38,6 +39,7 @@ protected CacheRemoveEntryInterceptor(CacheErrorHandler errorHandler) { @Override + @Nullable protected Object invoke( CacheOperationInvocationContext context, CacheOperationInvoker invoker) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java index cd3d91b37922..755a7374d294 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -146,7 +146,6 @@ private static CacheOperationInvoker.ThrowableWrapper rewriteCallStack( return new CacheOperationInvoker.ThrowableWrapper(clone); } - @SuppressWarnings("unchecked") @Nullable private static T cloneException(T exception) { try { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java index 4bde292d8fa5..f6f84d360467 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -232,7 +232,7 @@ protected KeyGenerator getDefaultKeyGenerator() { * Only resolve the default exception cache resolver when an exception needs to be handled. *

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

This gives this weird situation of a perfectly valid configuration that breaks all * of a sudden because the JCache support is enabled. To avoid this we resolve the default diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java index 1dbf3c8ae6d4..de1ff1a293af 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java @@ -180,6 +180,7 @@ public CacheOperationInvokerAdapter(CacheOperationInvoker delegate) { } @Override + @Nullable public Object invoke() throws ThrowableWrapper { return invokeOperation(this.delegate); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java index 445a7ef82824..2aa8c2ddb9f0 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ public interface JCacheOperationSource { * Return the cache operations for this method, or {@code null} * if the method contains no JSR-107 related metadata. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return the cache operation for this method, or {@code null} if none found */ diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java index 34693866eea2..a61779b05170 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import org.springframework.util.ObjectUtils; /** - * A Pointcut that matches if the underlying {@link JCacheOperationSource} + * A {@code Pointcut} that matches if the underlying {@link JCacheOperationSource} * has an operation for a given method. * * @author Stephane Nicoll diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java index 1aa569546131..70eca3aad5e3 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java @@ -24,6 +24,7 @@ import org.springframework.cache.interceptor.BasicOperation; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.lang.Nullable; /** * A simple {@link CacheResolver} that resolves the exception cache @@ -41,6 +42,7 @@ public SimpleExceptionCacheResolver(CacheManager cacheManager) { } @Override + @Nullable protected Collection getCacheNames(CacheOperationInvocationContext context) { BasicOperation operation = context.getOperation(); if (!(operation instanceof CacheResultOperation cacheResultOperation)) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java index 33571ffc905b..45b5870dfecc 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package org.springframework.cache.transaction; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; import org.springframework.cache.Cache; import org.springframework.lang.Nullable; @@ -81,6 +83,7 @@ public ValueWrapper get(Object key) { } @Override + @Nullable public T get(Object key, @Nullable Class type) { return this.targetCache.get(key, type); } @@ -91,6 +94,17 @@ public T get(Object key, Callable valueLoader) { return this.targetCache.get(key, valueLoader); } + @Override + @Nullable + public CompletableFuture retrieve(Object key) { + return this.targetCache.retrieve(key); + } + + @Override + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + return this.targetCache.retrieve(key, valueLoader); + } + @Override public void put(final Object key, @Nullable final Object value) { if (TransactionSynchronizationManager.isSynchronizationActive()) { diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailSender.java b/spring-context-support/src/main/java/org/springframework/mail/MailSender.java index 6fd8265f59c2..d0d95aab9478 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/MailSender.java +++ b/spring-context-support/src/main/java/org/springframework/mail/MailSender.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,9 @@ public interface MailSender { * @throws MailAuthenticationException in case of authentication failure * @throws MailSendException in case of failure when sending the message */ - void send(SimpleMailMessage simpleMessage) throws MailException; + default void send(SimpleMailMessage simpleMessage) throws MailException { + send(new SimpleMailMessage[] {simpleMessage}); + } /** * Send the given array of simple mail messages in batch. diff --git a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java index 81657e90453b..d86db63adb0a 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java +++ b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java @@ -236,14 +236,8 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(this.from); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.replyTo); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.to); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.cc); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.bcc); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.sentDate); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.subject); - return hashCode; + return ObjectUtils.nullSafeHash(this.from, this.replyTo, this.to, this.cc, + this.bcc, this.sentDate, this.subject); } @Override diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java index 61f4ecb01d77..6c696d13f1b5 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,15 @@ package org.springframework.mail.javamail; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import org.springframework.mail.MailException; +import org.springframework.mail.MailParseException; +import org.springframework.mail.MailPreparationException; import org.springframework.mail.MailSender; /** @@ -92,7 +97,9 @@ public interface JavaMailSender extends MailSender { * in case of failure when sending the message * @see #createMimeMessage */ - void send(MimeMessage mimeMessage) throws MailException; + default void send(MimeMessage mimeMessage) throws MailException { + send(new MimeMessage[] {mimeMessage}); + } /** * Send the given array of JavaMail MIME messages in batch. @@ -121,7 +128,9 @@ public interface JavaMailSender extends MailSender { * @throws org.springframework.mail.MailSendException * in case of failure when sending the message */ - void send(MimeMessagePreparator mimeMessagePreparator) throws MailException; + default void send(MimeMessagePreparator mimeMessagePreparator) throws MailException { + send(new MimeMessagePreparator[] {mimeMessagePreparator}); + } /** * Send the JavaMail MIME messages prepared by the given MimeMessagePreparators. @@ -138,6 +147,25 @@ public interface JavaMailSender extends MailSender { * @throws org.springframework.mail.MailSendException * in case of failure when sending a message */ - void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException; + default void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException { + try { + List mimeMessages = new ArrayList<>(mimeMessagePreparators.length); + for (MimeMessagePreparator preparator : mimeMessagePreparators) { + MimeMessage mimeMessage = createMimeMessage(); + preparator.prepare(mimeMessage); + mimeMessages.add(mimeMessage); + } + send(mimeMessages.toArray(new MimeMessage[0])); + } + catch (MailException ex) { + throw ex; + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + catch (Exception ex) { + throw new MailPreparationException(ex); + } + } } diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java index eb8a48834f85..adc888dae95f 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java @@ -37,7 +37,6 @@ import org.springframework.mail.MailAuthenticationException; import org.springframework.mail.MailException; import org.springframework.mail.MailParseException; -import org.springframework.mail.MailPreparationException; import org.springframework.mail.MailSendException; import org.springframework.mail.SimpleMailMessage; import org.springframework.util.Assert; @@ -307,11 +306,6 @@ public FileTypeMap getDefaultFileTypeMap() { // Implementation of MailSender //--------------------------------------------------------------------- - @Override - public void send(SimpleMailMessage simpleMessage) throws MailException { - send(new SimpleMailMessage[] {simpleMessage}); - } - @Override public void send(SimpleMailMessage... simpleMessages) throws MailException { List mimeMessages = new ArrayList<>(simpleMessages.length); @@ -351,43 +345,11 @@ public MimeMessage createMimeMessage(InputStream contentStream) throws MailExcep } } - @Override - public void send(MimeMessage mimeMessage) throws MailException { - send(new MimeMessage[] {mimeMessage}); - } - @Override public void send(MimeMessage... mimeMessages) throws MailException { doSend(mimeMessages, null); } - @Override - public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException { - send(new MimeMessagePreparator[] {mimeMessagePreparator}); - } - - @Override - public void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException { - try { - List mimeMessages = new ArrayList<>(mimeMessagePreparators.length); - for (MimeMessagePreparator preparator : mimeMessagePreparators) { - MimeMessage mimeMessage = createMimeMessage(); - preparator.prepare(mimeMessage); - mimeMessages.add(mimeMessage); - } - send(mimeMessages.toArray(new MimeMessage[0])); - } - catch (MailException ex) { - throw ex; - } - catch (MessagingException ex) { - throw new MailParseException(ex); - } - catch (Exception ex) { - throw new MailPreparationException(ex); - } - } - /** * Validate that this instance can connect to the server that it is configured * for. Throws a {@link MessagingException} if the connection attempt failed. @@ -528,7 +490,7 @@ protected Transport connectTransport() throws MessagingException { * @see #getProtocol() */ protected Transport getTransport(Session session) throws NoSuchProviderException { - String protocol = getProtocol(); + String protocol = getProtocol(); if (protocol == null) { protocol = session.getProperty("mail.transport.protocol"); if (protocol == null) { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java index c0a39c081fda..73d12fab6927 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java @@ -30,7 +30,6 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.Constants; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -48,6 +47,7 @@ * instead of registering the JobDetail separately. * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.1 * @see #setName * @see #setGroup @@ -58,8 +58,16 @@ */ public class CronTriggerFactoryBean implements FactoryBean, BeanNameAware, InitializingBean { - /** Constants for the CronTrigger class. */ - private static final Constants constants = new Constants(CronTrigger.class); + /** + * Map of constant names to constant values for the misfire instruction constants + * defined in {@link org.quartz.Trigger} and {@link org.quartz.CronTrigger}. + */ + private static final Map constants = Map.of( + "MISFIRE_INSTRUCTION_SMART_POLICY", CronTrigger.MISFIRE_INSTRUCTION_SMART_POLICY, + "MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY", CronTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY, + "MISFIRE_INSTRUCTION_FIRE_ONCE_NOW", CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW, + "MISFIRE_INSTRUCTION_DO_NOTHING", CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING + ); @Nullable @@ -199,6 +207,8 @@ public void setPriority(int priority) { * Specify the misfire instruction for this trigger. */ public void setMisfireInstruction(int misfireInstruction) { + Assert.isTrue(constants.containsValue(misfireInstruction), + "Only values of misfire instruction constants allowed"); this.misfireInstruction = misfireInstruction; } @@ -213,7 +223,10 @@ public void setMisfireInstruction(int misfireInstruction) { * @see org.quartz.CronTrigger#MISFIRE_INSTRUCTION_DO_NOTHING */ public void setMisfireInstructionName(String constantName) { - this.misfireInstruction = constants.asNumber(constantName).intValue(); + Assert.hasText(constantName, "'constantName' must not be null or blank"); + Integer misfireInstruction = constants.get(constantName); + Assert.notNull(misfireInstruction, "Only misfire instruction constants allowed"); + this.misfireInstruction = misfireInstruction; } /** diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java index cace9ab82e4b..d692c4bf74f8 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java @@ -199,6 +199,7 @@ protected void postProcessJobDetail(JobDetail jobDetail) { * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. */ @Override + @Nullable public Class getTargetClass() { Class targetClass = super.getTargetClass(); if (targetClass == null && this.targetBeanName != null) { @@ -212,6 +213,7 @@ public Class getTargetClass() { * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. */ @Override + @Nullable public Object getTargetObject() { Object targetObject = super.getTargetObject(); if (targetObject == null && this.targetBeanName != null) { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java index 82015a57549f..8b69a7d50e4c 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -736,27 +736,24 @@ protected void startScheduler(final Scheduler scheduler, final int startupDelay) } // Not using the Quartz startDelayed method since we explicitly want a daemon // thread here, not keeping the JVM alive in case of all other threads ending. - Thread schedulerThread = new Thread() { - @Override - public void run() { - try { - TimeUnit.SECONDS.sleep(startupDelay); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - // simply proceed - } - if (logger.isInfoEnabled()) { - logger.info("Starting Quartz Scheduler now, after delay of " + startupDelay + " seconds"); - } - try { - scheduler.start(); - } - catch (SchedulerException ex) { - throw new SchedulingException("Could not start Quartz Scheduler after delay", ex); - } + Thread schedulerThread = new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(startupDelay); } - }; + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + // simply proceed + } + if (logger.isInfoEnabled()) { + logger.info("Starting Quartz Scheduler now, after delay of " + startupDelay + " seconds"); + } + try { + scheduler.start(); + } + catch (SchedulerException ex) { + throw new SchedulingException("Could not start Quartz Scheduler after delay", ex); + } + }); schedulerThread.setName("Quartz Scheduler [" + scheduler.getSchedulerName() + "]"); schedulerThread.setDaemon(true); schedulerThread.start(); diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java index c2d727a45588..ee5e43c05c39 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,11 +22,12 @@ import org.springframework.aot.hint.TypeHint.Builder; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** * {@link RuntimeHintsRegistrar} implementation that makes sure {@link SchedulerFactoryBean} - * reflection entries are registered. + * reflection hints are registered. * * @author Sebastien Deleuze * @author Stephane Nicoll @@ -36,11 +37,11 @@ class SchedulerFactoryBeanRuntimeHints implements RuntimeHintsRegistrar { private static final String SCHEDULER_FACTORY_CLASS_NAME = "org.quartz.impl.StdSchedulerFactory"; - private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar(); + private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { if (!ClassUtils.isPresent(SCHEDULER_FACTORY_CLASS_NAME, classLoader)) { return; } @@ -48,7 +49,7 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { .registerType(TypeReference.of(SCHEDULER_FACTORY_CLASS_NAME), this::typeHint) .registerTypes(TypeReference.listOf(ResourceLoaderClassLoadHelper.class, LocalTaskExecutorThreadPool.class, LocalDataSourceJobStore.class), this::typeHint); - this.reflectiveRegistrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class); + registrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class); } private void typeHint(Builder typeHint) { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java index 8be2a3534ac1..811d76c9ef38 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,12 +77,6 @@ public void execute(Runnable task) { } } - @Deprecated - @Override - public void execute(Runnable task, long startTimeout) { - execute(task); - } - @Override public Future submit(Runnable task) { FutureTask future = new FutureTask<>(task, null); diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java index bab87a5a29a0..e16a32f59677 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java @@ -28,7 +28,6 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.Constants; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -46,6 +45,7 @@ * instead of registering the JobDetail separately. * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.1 * @see #setName * @see #setGroup @@ -56,9 +56,26 @@ */ public class SimpleTriggerFactoryBean implements FactoryBean, BeanNameAware, InitializingBean { - /** Constants for the SimpleTrigger class. */ - private static final Constants constants = new Constants(SimpleTrigger.class); - + /** + * Map of constant names to constant values for the misfire instruction constants + * defined in {@link org.quartz.Trigger} and {@link org.quartz.SimpleTrigger}. + */ + private static final Map constants = Map.of( + "MISFIRE_INSTRUCTION_SMART_POLICY", + SimpleTrigger.MISFIRE_INSTRUCTION_SMART_POLICY, + "MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY", + SimpleTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY, + "MISFIRE_INSTRUCTION_FIRE_NOW", + SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW, + "MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT", + SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT, + "MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT", + SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT, + "MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT", + SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT, + "MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT", + SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT + ); @Nullable private String name; @@ -187,6 +204,8 @@ public void setPriority(int priority) { * Specify the misfire instruction for this trigger. */ public void setMisfireInstruction(int misfireInstruction) { + Assert.isTrue(constants.containsValue(misfireInstruction), + "Only values of misfire instruction constants allowed"); this.misfireInstruction = misfireInstruction; } @@ -204,7 +223,10 @@ public void setMisfireInstruction(int misfireInstruction) { * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT */ public void setMisfireInstructionName(String constantName) { - this.misfireInstruction = constants.asNumber(constantName).intValue(); + Assert.hasText(constantName, "'constantName' must not be null or blank"); + Integer misfireInstruction = constants.get(constantName); + Assert.notNull(misfireInstruction, "Only misfire instruction constants allowed"); + this.misfireInstruction = misfireInstruction; } /** diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java index 796f56ce0257..2d48a2258ae0 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java @@ -22,7 +22,6 @@ import org.springframework.beans.BeanWrapper; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessorFactory; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.lang.Nullable; @@ -87,9 +86,7 @@ public void setSchedulerContext(SchedulerContext schedulerContext) { @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Object job = (this.applicationContext != null ? - // to be replaced with createBean(Class) in 6.1 - this.applicationContext.getAutowireCapableBeanFactory().createBean( - bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) : + this.applicationContext.getAutowireCapableBeanFactory().createBean(bundle.getJobDetail().getJobClass()) : super.createJobInstance(bundle)); if (isEligibleForPropertyPopulation(job)) { diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java index bc3ddc27e01b..c3df6a706cf9 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,9 +41,11 @@ import org.springframework.util.CollectionUtils; /** - * Factory that configures a FreeMarker Configuration. Can be used standalone, but - * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a - * Configuration as bean reference, or FreeMarkerConfigurer for web views. + * Factory that configures a FreeMarker {@link Configuration}. + * + *

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

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

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

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

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

Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * * @author Darren Davison * @author Juergen Hoeller + * @author Sam Brannen * @since 03.03.2004 * @see #setConfigLocation * @see #setFreemarkerSettings @@ -107,7 +110,7 @@ public class FreeMarkerConfigurationFactory { /** * Set the location of the FreeMarker config file. - * Alternatively, you can specify all setting locally. + *

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

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

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

Note that the encoding is not used for template rendering. Instead, an + * explicit encoding must be specified for the rendering process — for + * example, via Spring's {@code FreeMarkerView} or {@code FreeMarkerViewResolver}. * @see freemarker.template.Configuration#setDefaultEncoding * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setContentType + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver#setContentType */ public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } /** - * Set a List of {@code TemplateLoader}s that will be used to search - * for templates. For example, one or more custom loaders such as database - * loaders could be configured and injected here. - *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File system access enables hot detection of template changes. *

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

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

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

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

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

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

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

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

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

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

If more than one TemplateLoader has been registered, a FreeMarker + * {@link MultiTemplateLoader} will be created. + * @param templateLoaders the final List of {@code TemplateLoader} instances * @return the aggregate TemplateLoader */ @Nullable @@ -404,10 +418,10 @@ protected TemplateLoader getAggregateTemplateLoader(List templat /** * To be overridden by subclasses that want to perform custom - * post-processing of the Configuration object after this factory + * post-processing of the {@link Configuration} object after this factory * performed its default initialization. - *

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

Called by {@link #createConfiguration()}. + * @param config the current {@code Configuration} object * @throws IOException if a config file wasn't found * @throws TemplateException on FreeMarker initialization failure * @see #createConfiguration() diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java index 5639ac06a91f..3aac7d4df215 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,22 +27,25 @@ import org.springframework.lang.Nullable; /** - * Factory bean that creates a FreeMarker Configuration and provides it as - * bean reference. This bean is intended for any kind of usage of FreeMarker - * in application code, e.g. for generating email content. For web views, - * FreeMarkerConfigurer is used to set up a FreeMarkerConfigurationFactory. + * Factory bean that creates a FreeMarker {@link Configuration} and provides it + * as a bean reference. * - * The simplest way to use this class is to specify just a "templateLoaderPath"; + *

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

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

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

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

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

Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * * @author Darren Davison * @since 03.03.2004 diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java index e1ca06bf99e3..022ebd9bdec3 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,8 @@ /** * Utility class for working with FreeMarker. - * Provides convenience methods to process a FreeMarker template with a model. + * + *

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

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

When using this method to prepare text for a mail to be sent with Spring's * mail support, consider wrapping IO/TemplateException in MailPreparationException. * @param model the model object, typically a Map that contains model names * as keys and model objects as values - * @return the result as String + * @return the result as a String * @throws IOException if the template wasn't found or couldn't be read * @throws freemarker.template.TemplateException if rendering failed * @see org.springframework.mail.MailPreparationException diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java index d4140aa681f1..1749b0a37d94 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,11 @@ import org.springframework.lang.Nullable; /** - * FreeMarker {@link TemplateLoader} adapter that loads via a Spring {@link ResourceLoader}. - * Used by {@link FreeMarkerConfigurationFactory} for any resource loader path that cannot - * be resolved to a {@link java.io.File}. + * FreeMarker {@link TemplateLoader} adapter that loads template files via a + * Spring {@link ResourceLoader}. + * + *

Used by {@link FreeMarkerConfigurationFactory} for any resource loader path + * that cannot be resolved to a {@link java.io.File}. * * @author Juergen Hoeller * @since 14.03.2004 @@ -48,7 +50,7 @@ public class SpringTemplateLoader implements TemplateLoader { /** - * Create a new SpringTemplateLoader. + * Create a new {@code SpringTemplateLoader}. * @param resourceLoader the Spring ResourceLoader to use * @param templateLoaderPath the template loader path to use */ diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index adb2838fd64e..c88b3c8d2b24 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.cache.caffeine; +import java.util.concurrent.CompletableFuture; + import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.CaffeineSpec; @@ -23,9 +25,11 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.cache.support.SimpleValueWrapper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; /** @@ -33,11 +37,11 @@ * @author Juergen Hoeller * @author Stephane Nicoll */ -public class CaffeineCacheManagerTests { +class CaffeineCacheManagerTests { @Test @SuppressWarnings("cast") - public void testDynamicMode() { + void dynamicMode() { CacheManager cm = new CaffeineCacheManager(); Cache cache1 = cm.getCache("c1"); @@ -53,6 +57,12 @@ public void testDynamicMode() { Cache cache3again = cm.getCache("c3"); assertThat(cache3).isSameAs(cache3again); + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key1")); + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key2")); + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key3")); + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key3", + () -> CompletableFuture.completedFuture("value3"))); + cache1.put("key1", "value1"); assertThat(cache1.get("key1").get()).isEqualTo("value1"); cache1.put("key2", 2); @@ -69,7 +79,8 @@ public void testDynamicMode() { } @Test - public void testStaticMode() { + @SuppressWarnings("cast") + void staticMode() { CaffeineCacheManager cm = new CaffeineCacheManager("c1", "c2"); Cache cache1 = cm.getCache("c1"); @@ -91,6 +102,17 @@ public void testStaticMode() { assertThat(cache1.get("key3").get()).isNull(); cache1.evict("key3"); assertThat(cache1.get("key3")).isNull(); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + cache1.evict("key3"); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); + + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key1")); + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key2")); + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key3")); + assertThatIllegalStateException().isThrownBy(() -> cache1.retrieve("key3", + () -> CompletableFuture.completedFuture("value3"))); cm.setAllowNullValues(false); Cache cache1x = cm.getCache("c1"); @@ -117,7 +139,94 @@ public void testStaticMode() { } @Test - public void changeCaffeineRecreateCache() { + @SuppressWarnings("cast") + void asyncMode() { + CaffeineCacheManager cm = new CaffeineCacheManager(); + cm.setAsyncCacheMode(true); + + Cache cache1 = cm.getCache("c1"); + assertThat(cache1).isInstanceOf(CaffeineCache.class); + Cache cache1again = cm.getCache("c1"); + assertThat(cache1).isSameAs(cache1again); + Cache cache2 = cm.getCache("c2"); + assertThat(cache2).isInstanceOf(CaffeineCache.class); + Cache cache2again = cm.getCache("c2"); + assertThat(cache2).isSameAs(cache2again); + Cache cache3 = cm.getCache("c3"); + assertThat(cache3).isInstanceOf(CaffeineCache.class); + Cache cache3again = cm.getCache("c3"); + assertThat(cache3).isSameAs(cache3again); + + cache1.put("key1", "value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + cache1.put("key2", 2); + assertThat(cache1.get("key2").get()).isEqualTo(2); + cache1.put("key3", null); + assertThat(cache1.get("key3").get()).isNull(); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + cache1.evict("key3"); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); + + assertThat(cache1.retrieve("key1").join()).isEqualTo(new SimpleValueWrapper("value1")); + assertThat(cache1.retrieve("key2").join()).isEqualTo(new SimpleValueWrapper(2)); + assertThat(cache1.retrieve("key3").join()).isEqualTo(new SimpleValueWrapper(null)); + cache1.evict("key3"); + assertThat(cache1.retrieve("key3")).isNull(); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) + .isEqualTo("value3"); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) + .isEqualTo("value3"); + cache1.evict("key3"); + assertThat(cache1.retrieve("key3")).isNull(); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture(null)).join()).isNull(); + assertThat(cache1.retrieve("key3").join()).isEqualTo(new SimpleValueWrapper(null)); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture(null)).join()).isNull(); + } + + @Test + void asyncModeWithoutNullValues() { + CaffeineCacheManager cm = new CaffeineCacheManager(); + cm.setAsyncCacheMode(true); + cm.setAllowNullValues(false); + + Cache cache1 = cm.getCache("c1"); + assertThat(cache1).isInstanceOf(CaffeineCache.class); + Cache cache1again = cm.getCache("c1"); + assertThat(cache1).isSameAs(cache1again); + Cache cache2 = cm.getCache("c2"); + assertThat(cache2).isInstanceOf(CaffeineCache.class); + Cache cache2again = cm.getCache("c2"); + assertThat(cache2).isSameAs(cache2again); + Cache cache3 = cm.getCache("c3"); + assertThat(cache3).isInstanceOf(CaffeineCache.class); + Cache cache3again = cm.getCache("c3"); + assertThat(cache3).isSameAs(cache3again); + + cache1.put("key1", "value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + cache1.put("key2", 2); + assertThat(cache1.get("key2").get()).isEqualTo(2); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + cache1.evict("key3"); + + assertThat(cache1.retrieve("key1").join()).isEqualTo("value1"); + assertThat(cache1.retrieve("key2").join()).isEqualTo(2); + assertThat(cache1.retrieve("key3")).isNull(); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) + .isEqualTo("value3"); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) + .isEqualTo("value3"); + } + + @Test + void changeCaffeineRecreateCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); Cache cache1 = cm.getCache("c1"); @@ -132,7 +241,7 @@ public void changeCaffeineRecreateCache() { } @Test - public void changeCaffeineSpecRecreateCache() { + void changeCaffeineSpecRecreateCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); Cache cache1 = cm.getCache("c1"); @@ -142,7 +251,7 @@ public void changeCaffeineSpecRecreateCache() { } @Test - public void changeCacheSpecificationRecreateCache() { + void changeCacheSpecificationRecreateCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); Cache cache1 = cm.getCache("c1"); @@ -152,11 +261,10 @@ public void changeCacheSpecificationRecreateCache() { } @Test - public void changeCacheLoaderRecreateCache() { + void changeCacheLoaderRecreateCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); Cache cache1 = cm.getCache("c1"); - @SuppressWarnings("unchecked") CacheLoader loader = mock(); cm.setCacheLoader(loader); @@ -169,7 +277,7 @@ public void changeCacheLoaderRecreateCache() { } @Test - public void setCacheNameNullRestoreDynamicMode() { + void setCacheNameNullRestoreDynamicMode() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); assertThat(cm.getCache("someCache")).isNull(); cm.setCacheNames(null); @@ -177,7 +285,7 @@ public void setCacheNameNullRestoreDynamicMode() { } @Test - public void cacheLoaderUseLoadingCache() { + void cacheLoaderUseLoadingCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); cm.setCacheLoader(key -> { if ("ping".equals(key)) { @@ -195,7 +303,7 @@ public void cacheLoaderUseLoadingCache() { } @Test - public void customCacheRegistration() { + void customCacheRegistration() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); com.github.benmanes.caffeine.cache.Cache nc = Caffeine.newBuilder().build(); cm.registerCustomCache("c2", nc); diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java index 7f760354d8b9..0afc36b44aaa 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ protected CaffeineCache getCache() { @Override protected CaffeineCache getCache(boolean allowNull) { - return allowNull ? this.cache : this.cacheNoNull; + return (allowNull ? this.cache : this.cacheNoNull); } @Override diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineReactiveCachingTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineReactiveCachingTests.java new file mode 100644 index 000000000000..f92e69c2f628 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineReactiveCachingTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache.caffeine; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for annotation-based caching methods that use reactive operators. + * + * @author Juergen Hoeller + * @since 6.1 + */ +class CaffeineReactiveCachingTests { + + @ParameterizedTest + @ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeConfig.class}) + void cacheHitDetermination(Class configClass) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Object key = new Object(); + + Long r1 = service.cacheFuture(key).join(); + Long r2 = service.cacheFuture(key).join(); + Long r3 = service.cacheFuture(key).join(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + key = new Object(); + + r1 = service.cacheMono(key).block(); + r2 = service.cacheMono(key).block(); + r3 = service.cacheMono(key).block(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + key = new Object(); + + r1 = service.cacheFlux(key).blockFirst(); + r2 = service.cacheFlux(key).blockFirst(); + r3 = service.cacheFlux(key).blockFirst(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + key = new Object(); + + List l1 = service.cacheFlux(key).collectList().block(); + List l2 = service.cacheFlux(key).collectList().block(); + List l3 = service.cacheFlux(key).collectList().block(); + + assertThat(l1).isNotNull(); + assertThat(l1).isEqualTo(l2).isEqualTo(l3); + + key = new Object(); + + r1 = service.cacheMono(key).block(); + r2 = service.cacheMono(key).block(); + r3 = service.cacheMono(key).block(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + // Same key as for Mono, reusing its cached value + + r1 = service.cacheFlux(key).blockFirst(); + r2 = service.cacheFlux(key).blockFirst(); + r3 = service.cacheFlux(key).blockFirst(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + ctx.close(); + } + + + @ParameterizedTest + @ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeConfig.class}) + void fluxCacheDoesntDependOnFirstRequest(Class configClass) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Object key = new Object(); + + List l1 = service.cacheFlux(key).take(1L, true).collectList().block(); + List l2 = service.cacheFlux(key).take(3L, true).collectList().block(); + List l3 = service.cacheFlux(key).collectList().block(); + + Long first = l1.get(0); + + assertThat(l1).as("l1").containsExactly(first); + assertThat(l2).as("l2").containsExactly(first, 0L, -1L); + assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L); + + ctx.close(); + } + + + @CacheConfig(cacheNames = "first") + static class ReactiveCacheableService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable + CompletableFuture cacheFuture(Object arg) { + return CompletableFuture.completedFuture(this.counter.getAndIncrement()); + } + + @Cacheable + Mono cacheMono(Object arg) { + // here counter not only reflects invocations of cacheMono but subscriptions to + // the returned Mono as well. See https://github.com/spring-projects/spring-framework/issues/32370 + return Mono.defer(() -> Mono.just(this.counter.getAndIncrement())); + } + + @Cacheable + Flux cacheFlux(Object arg) { + // here counter not only reflects invocations of cacheFlux but subscriptions to + // the returned Flux as well. See https://github.com/spring-projects/spring-framework/issues/32370 + return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L)); + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class AsyncCacheModeConfig { + + @Bean + CacheManager cacheManager() { + CaffeineCacheManager cm = new CaffeineCacheManager("first"); + cm.setAsyncCacheMode(true); + return cm; + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class AsyncCacheModeWithoutNullValuesConfig { + + @Bean + CacheManager cacheManager() { + CaffeineCacheManager ccm = new CaffeineCacheManager("first"); + ccm.setAsyncCacheMode(true); + ccm.setAllowNullValues(false); + return ccm; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java index 502b17f6dcdf..481bd9b912f5 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ /** * @author Stephane Nicoll */ -public class JCacheCacheManagerTests extends AbstractTransactionSupportingCacheManagerTests { +class JCacheCacheManagerTests extends AbstractTransactionSupportingCacheManagerTests { private CacheManagerMock cacheManagerMock; @@ -42,7 +42,7 @@ public class JCacheCacheManagerTests extends AbstractTransactionSupportingCacheM @BeforeEach - public void setupOnce() { + void setupOnce() { cacheManagerMock = new CacheManagerMock(); cacheManagerMock.addCache(CACHE_NAME); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java index e77e948c802b..61f2020d7111 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ * @author Stephane Nicoll * @author Juergen Hoeller */ -public class JCacheEhCacheAnnotationTests extends AbstractCacheAnnotationTests { +class JCacheEhCacheAnnotationTests extends AbstractCacheAnnotationTests { private final TransactionTemplate txTemplate = new TransactionTemplate(new CallCountingTransactionManager()); @@ -68,7 +68,7 @@ protected CachingProvider getCachingProvider() { } @AfterEach - public void shutdown() { + void shutdown() { if (jCacheManager != null) { jCacheManager.close(); } @@ -82,22 +82,22 @@ public void testCustomCacheManager() { } @Test - public void testEvictWithTransaction() { + void testEvictWithTransaction() { txTemplate.executeWithoutResult(s -> testEvict(this.cs, false)); } @Test - public void testEvictEarlyWithTransaction() { + void testEvictEarlyWithTransaction() { txTemplate.executeWithoutResult(s -> testEvictEarly(this.cs)); } @Test - public void testEvictAllWithTransaction() { + void testEvictAllWithTransaction() { txTemplate.executeWithoutResult(s -> testEvictAll(this.cs, false)); } @Test - public void testEvictAllEarlyWithTransaction() { + void testEvictAllEarlyWithTransaction() { txTemplate.executeWithoutResult(s -> testEvictAllEarly(this.cs)); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java index 3f56f2e06c5a..1adea115c396 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,13 +24,16 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.context.testfixture.cache.AbstractValueAdaptingCacheTests; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Stephane Nicoll */ -public class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests { +class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests { private CacheManager cacheManager; @@ -42,7 +45,7 @@ public class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests()); this.cacheManager.createCache(CACHE_NAME_NO_NULL, new MutableConfiguration<>()); @@ -58,7 +61,7 @@ protected CachingProvider getCachingProvider() { } @AfterEach - public void shutdown() { + void shutdown() { if (this.cacheManager != null) { this.cacheManager.close(); } @@ -71,7 +74,7 @@ protected JCacheCache getCache() { @Override protected JCacheCache getCache(boolean allowNull) { - return allowNull ? this.cache : this.cacheNoNull; + return (allowNull ? this.cache : this.cacheNoNull); } @Override @@ -79,4 +82,22 @@ protected Object getNativeCache() { return this.nativeCache; } + @Test + void testPutIfAbsentNullValue() { + JCacheCache cache = getCache(true); + + String key = createRandomKey(); + String value = null; + + assertThat(cache.get(key)).isNull(); + assertThat(cache.putIfAbsent(key, value)).isNull(); + assertThat(cache.get(key).get()).isEqualTo(value); + org.springframework.cache.Cache.ValueWrapper wrapper = cache.putIfAbsent(key, "anotherValue"); + // A value is set but is 'null' + assertThat(wrapper).isNotNull(); + assertThat(wrapper.get()).isNull(); + // not changed + assertThat(cache.get(key).get()).isEqualTo(value); + } + } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java index 62f94db25fcd..27384ef73e5d 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ /** * @author Stephane Nicoll */ -public class JCacheCustomInterceptorTests { +class JCacheCustomInterceptorTests { protected ConfigurableApplicationContext ctx; @@ -55,14 +55,14 @@ public class JCacheCustomInterceptorTests { @BeforeEach - public void setup() { + void setup() { ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class); cs = ctx.getBean("service", JCacheableService.class); exceptionCache = ctx.getBean("exceptionCache", Cache.class); } @AfterEach - public void tearDown() { + void tearDown() { if (ctx != null) { ctx.close(); } @@ -70,7 +70,7 @@ public void tearDown() { @Test - public void onlyOneInterceptorIsAvailable() { + void onlyOneInterceptorIsAvailable() { Map interceptors = ctx.getBeansOfType(JCacheInterceptor.class); assertThat(interceptors).as("Only one interceptor should be defined").hasSize(1); JCacheInterceptor interceptor = interceptors.values().iterator().next(); @@ -78,14 +78,14 @@ public void onlyOneInterceptorIsAvailable() { } @Test - public void customInterceptorAppliesWithRuntimeException() { + void customInterceptorAppliesWithRuntimeException() { Object o = cs.cacheWithException("id", true); // See TestCacheInterceptor assertThat(o).isEqualTo(55L); } @Test - public void customInterceptorAppliesWithCheckedException() { + void customInterceptorAppliesWithCheckedException() { assertThatRuntimeException() .isThrownBy(() -> cs.cacheWithCheckedException("id", true)) .withCauseExactlyInstanceOf(IOException.class); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java index 1dcc12540d1e..4cdfa12cec62 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ /** * @author Stephane Nicoll */ -public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { +class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { @@ -61,7 +61,7 @@ protected ApplicationContext getApplicationContext() { @Test - public void fullCachingConfig() throws Exception { + void fullCachingConfig() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(FullCachingConfig.class); @@ -75,7 +75,7 @@ public void fullCachingConfig() throws Exception { } @Test - public void emptyConfigSupport() { + void emptyConfigSupport() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(EmptyConfigSupportConfig.class); @@ -88,7 +88,7 @@ public void emptyConfigSupport() { } @Test - public void bothSetOnlyResolverIsUsed() { + void bothSetOnlyResolverIsUsed() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(FullCachingConfigSupport.class); @@ -100,7 +100,7 @@ public void bothSetOnlyResolverIsUsed() { } @Test - public void exceptionCacheResolverLazilyRequired() { + void exceptionCacheResolverLazilyRequired() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(NoExceptionCacheResolverConfig.class)) { DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); assertThat(cos.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java index 262689f83ad8..2e4800fac7bc 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ /** * @author Stephane Nicoll */ -public class JCacheNamespaceDrivenTests extends AbstractJCacheAnnotationTests { +class JCacheNamespaceDrivenTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { @@ -40,7 +40,7 @@ protected ApplicationContext getApplicationContext() { } @Test - public void cacheResolver() { + void cacheResolver() { ConfigurableApplicationContext context = new GenericXmlApplicationContext( "/org/springframework/cache/jcache/config/jCacheNamespaceDriven-resolver.xml"); @@ -50,7 +50,7 @@ public void cacheResolver() { } @Test - public void testCacheErrorHandler() { + void testCacheErrorHandler() { JCacheInterceptor ci = ctx.getBean(JCacheInterceptor.class); assertThat(ci.getErrorHandler()).isSameAs(ctx.getBean("errorHandler", CacheErrorHandler.class)); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java index f07e5214850e..615498040a49 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ /** * @author Stephane Nicoll */ -public class JCacheStandaloneConfigTests extends AbstractJCacheAnnotationTests { +class JCacheStandaloneConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java index fff00d06f04b..140c1d519c8e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,13 +42,10 @@ public abstract class AbstractCacheOperationTests> @Test - public void simple() { + void simple() { O operation = createSimpleOperation(); assertThat(operation.getCacheName()).as("Wrong cache name").isEqualTo("simpleCache"); - assertThat(operation.getAnnotations()).as("Unexpected number of annotation on " + operation.getMethod()) - .hasSize(1); - assertThat(operation.getAnnotations().iterator().next()).as("Wrong method annotation").isEqualTo(operation.getCacheAnnotation()); - + assertThat(operation.getAnnotations()).singleElement().isEqualTo(operation.getCacheAnnotation()); assertThat(operation.getCacheResolver()).as("cache resolver should be set").isNotNull(); } @@ -70,7 +67,7 @@ protected CacheMethodDetails create(Class annotatio private static String getCacheName(Annotation annotation) { Object cacheName = AnnotationUtils.getValue(annotation, "cacheName"); - return cacheName != null ? cacheName.toString() : "test"; + return (cacheName != null ? cacheName.toString() : "test"); } } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java index e85c27507862..4ced7de3d6a9 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ /** * @author Stephane Nicoll */ -public class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { +class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { private final DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); @@ -54,7 +54,7 @@ public class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { @BeforeEach - public void setup() { + void setup() { source.setCacheResolver(defaultCacheResolver); source.setExceptionCacheResolver(defaultExceptionCacheResolver); source.setKeyGenerator(defaultKeyGenerator); @@ -63,14 +63,14 @@ public void setup() { @Test - public void cache() { + void cache() { CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class); assertDefaults(op); assertThat(op.getExceptionCacheResolver()).as("Exception caching not enabled so resolver should not be set").isNull(); } @Test - public void cacheWithException() { + void cacheWithException() { CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class, boolean.class); assertDefaults(op); assertThat(op.getExceptionCacheResolver()).isEqualTo(defaultExceptionCacheResolver); @@ -78,41 +78,41 @@ public void cacheWithException() { } @Test - public void put() { + void put() { CachePutOperation op = getDefaultCacheOperation(CachePutOperation.class, String.class, Object.class); assertDefaults(op); } @Test - public void remove() { + void remove() { CacheRemoveOperation op = getDefaultCacheOperation(CacheRemoveOperation.class, String.class); assertDefaults(op); } @Test - public void removeAll() { + void removeAll() { CacheRemoveAllOperation op = getDefaultCacheOperation(CacheRemoveAllOperation.class); assertThat(op.getCacheResolver()).isEqualTo(defaultCacheResolver); } @Test - public void noAnnotation() { + void noAnnotation() { assertThat(getCacheOperation(AnnotatedJCacheableService.class, this.cacheName)).isNull(); } @Test - public void multiAnnotations() { + void multiAnnotations() { assertThatIllegalStateException().isThrownBy(() -> getCacheOperation(InvalidCases.class, this.cacheName)); } @Test - public void defaultCacheNameWithCandidate() { + void defaultCacheNameWithCandidate() { Method method = ReflectionUtils.findMethod(Object.class, "toString"); assertThat(source.determineCacheName(method, null, "foo")).isEqualTo("foo"); } @Test - public void defaultCacheNameWithDefaults() { + void defaultCacheNameWithDefaults() { Method method = ReflectionUtils.findMethod(Object.class, "toString"); CacheDefaults mock = mock(); given(mock.cacheName()).willReturn(""); @@ -120,19 +120,19 @@ public void defaultCacheNameWithDefaults() { } @Test - public void defaultCacheNameNoDefaults() { + void defaultCacheNameNoDefaults() { Method method = ReflectionUtils.findMethod(Object.class, "toString"); assertThat(source.determineCacheName(method, null, "")).isEqualTo("java.lang.Object.toString()"); } @Test - public void defaultCacheNameWithParameters() { + void defaultCacheNameWithParameters() { Method method = ReflectionUtils.findMethod(Comparator.class, "compare", Object.class, Object.class); assertThat(source.determineCacheName(method, null, "")).isEqualTo("java.util.Comparator.compare(java.lang.Object,java.lang.Object)"); } @Test - public void customCacheResolver() { + void customCacheResolver() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomService.class, this.cacheName, Long.class); assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); @@ -142,7 +142,7 @@ public void customCacheResolver() { } @Test - public void customKeyGenerator() { + void customKeyGenerator() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomService.class, this.cacheName, Long.class); assertThat(operation.getCacheResolver()).isEqualTo(defaultCacheResolver); @@ -151,7 +151,7 @@ public void customKeyGenerator() { } @Test - public void customKeyGeneratorSpringBean() { + void customKeyGeneratorSpringBean() { TestableCacheKeyGenerator bean = new TestableCacheKeyGenerator(); beanFactory.registerSingleton("fooBar", bean); CacheResultOperation operation = @@ -164,7 +164,7 @@ public void customKeyGeneratorSpringBean() { } @Test - public void customKeyGeneratorAndCacheResolver() { + void customKeyGeneratorAndCacheResolver() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomServiceWithDefaults.class, this.cacheName, Long.class); assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); @@ -173,7 +173,7 @@ public void customKeyGeneratorAndCacheResolver() { } @Test - public void customKeyGeneratorAndCacheResolverWithExceptionName() { + void customKeyGeneratorAndCacheResolverWithExceptionName() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomServiceWithDefaults.class, this.cacheName, Long.class); assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java index dee2f07ba3d0..ce3437751a1f 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ /** * @author Stephane Nicoll */ -public class CachePutOperationTests extends AbstractCacheOperationTests { +class CachePutOperationTests extends AbstractCacheOperationTests { @Override protected CachePutOperation createSimpleOperation() { @@ -41,7 +41,7 @@ protected CachePutOperation createSimpleOperation() { } @Test - public void simplePut() { + void simplePut() { CachePutOperation operation = createSimpleOperation(); CacheInvocationParameter[] allParameters = operation.getAllParameters(2L, sampleInstance); @@ -55,7 +55,7 @@ public void simplePut() { } @Test - public void noCacheValue() { + void noCacheValue() { CacheMethodDetails methodDetails = create(CachePut.class, SampleObject.class, "noCacheValue", Long.class); @@ -64,7 +64,7 @@ public void noCacheValue() { } @Test - public void multiCacheValues() { + void multiCacheValues() { CacheMethodDetails methodDetails = create(CachePut.class, SampleObject.class, "multiCacheValues", Long.class, SampleObject.class, SampleObject.class); @@ -73,7 +73,7 @@ public void multiCacheValues() { } @Test - public void invokeWithWrongParameters() { + void invokeWithWrongParameters() { CachePutOperation operation = createSimpleOperation(); assertThatIllegalStateException().isThrownBy(() -> @@ -81,7 +81,7 @@ public void invokeWithWrongParameters() { } @Test - public void fullPutConfig() { + void fullPutConfig() { CacheMethodDetails methodDetails = create(CachePut.class, SampleObject.class, "fullPutConfig", Long.class, SampleObject.class); CachePutOperation operation = createDefaultOperation(methodDetails); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java index 083ae9048b14..e614b970e9d8 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ /** * @author Stephane Nicoll */ -public class CacheRemoveAllOperationTests extends AbstractCacheOperationTests { +class CacheRemoveAllOperationTests extends AbstractCacheOperationTests { @Override protected CacheRemoveAllOperation createSimpleOperation() { @@ -38,7 +38,7 @@ protected CacheRemoveAllOperation createSimpleOperation() { } @Test - public void simpleRemoveAll() { + void simpleRemoveAll() { CacheRemoveAllOperation operation = createSimpleOperation(); CacheInvocationParameter[] allParameters = operation.getAllParameters(); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java index 63c792d74c92..979edc1ec00d 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ /** * @author Stephane Nicoll */ -public class CacheRemoveOperationTests extends AbstractCacheOperationTests { +class CacheRemoveOperationTests extends AbstractCacheOperationTests { @Override protected CacheRemoveOperation createSimpleOperation() { @@ -38,7 +38,7 @@ protected CacheRemoveOperation createSimpleOperation() { } @Test - public void simpleRemove() { + void simpleRemove() { CacheRemoveOperation operation = createSimpleOperation(); CacheInvocationParameter[] allParameters = operation.getAllParameters(2L); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java index 9692d0e7edd9..1c6570009d2e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,10 +38,10 @@ /** * @author Stephane Nicoll */ -public class CacheResolverAdapterTests extends AbstractJCacheTests { +class CacheResolverAdapterTests extends AbstractJCacheTests { @Test - public void resolveSimpleCache() throws Exception { + void resolveSimpleCache() throws Exception { DefaultCacheInvocationContext dummyContext = createDummyContext(); CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, "testCache")); Collection caches = adapter.resolveCaches(dummyContext); @@ -51,7 +51,7 @@ public void resolveSimpleCache() throws Exception { } @Test - public void resolveUnknownCache() throws Exception { + void resolveUnknownCache() throws Exception { DefaultCacheInvocationContext dummyContext = createDummyContext(); CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, null)); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java index 0764e1788e45..34477dfe9b3e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ /** * @author Stephane Nicoll */ -public class CacheResultOperationTests extends AbstractCacheOperationTests { +class CacheResultOperationTests extends AbstractCacheOperationTests { @Override protected CacheResultOperation createSimpleOperation() { @@ -47,7 +47,7 @@ protected CacheResultOperation createSimpleOperation() { } @Test - public void simpleGet() { + void simpleGet() { CacheResultOperation operation = createSimpleOperation(); assertThat(operation.getKeyGenerator()).isNotNull(); @@ -66,7 +66,7 @@ public void simpleGet() { } @Test - public void multiParameterKey() { + void multiParameterKey() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "multiKeysGet", Long.class, Boolean.class, String.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -78,7 +78,7 @@ public void multiParameterKey() { } @Test - public void invokeWithWrongParameters() { + void invokeWithWrongParameters() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "anotherSimpleGet", String.class, Long.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -89,7 +89,7 @@ public void invokeWithWrongParameters() { } @Test - public void tooManyKeyValues() { + void tooManyKeyValues() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "anotherSimpleGet", String.class, Long.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -100,7 +100,7 @@ public void tooManyKeyValues() { } @Test - public void annotatedGet() { + void annotatedGet() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "annotatedGet", Long.class, String.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -116,7 +116,7 @@ public void annotatedGet() { } @Test - public void fullGetConfig() { + void fullGetConfig() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "fullGetConfig", Long.class); CacheResultOperation operation = createDefaultOperation(methodDetails); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java index 3626d9f90f21..b26719570c0d 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ /** * @author Stephane Nicoll */ -public class JCacheErrorHandlerTests { +class JCacheErrorHandlerTests { private Cache cache; @@ -61,7 +61,7 @@ public class JCacheErrorHandlerTests { @BeforeEach - public void setup() { + void setup() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); this.cache = context.getBean("mockCache", Cache.class); this.errorCache = context.getBean("mockErrorCache", Cache.class); @@ -72,7 +72,7 @@ public void setup() { @Test - public void getFail() { + void getFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); Object key = SimpleKeyGenerator.generateKey(0L); willThrow(exception).given(this.cache).get(key); @@ -82,7 +82,7 @@ public void getFail() { } @Test - public void getPutNewElementFail() { + void getPutNewElementFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); Object key = SimpleKeyGenerator.generateKey(0L); given(this.cache.get(key)).willReturn(null); @@ -93,7 +93,7 @@ public void getPutNewElementFail() { } @Test - public void getFailPutExceptionFail() { + void getFailPutExceptionFail() { UnsupportedOperationException exceptionOnPut = new UnsupportedOperationException("Test exception on put"); Object key = SimpleKeyGenerator.generateKey(0L); given(this.cache.get(key)).willReturn(null); @@ -110,7 +110,7 @@ public void getFailPutExceptionFail() { } @Test - public void putFail() { + void putFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); Object key = SimpleKeyGenerator.generateKey(0L); willThrow(exception).given(this.cache).put(key, 234L); @@ -120,7 +120,7 @@ public void putFail() { } @Test - public void evictFail() { + void evictFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); Object key = SimpleKeyGenerator.generateKey(0L); willThrow(exception).given(this.cache).evict(key); @@ -130,7 +130,7 @@ public void evictFail() { } @Test - public void clearFail() { + void clearFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); willThrow(exception).given(this.cache).clear(); @@ -183,7 +183,7 @@ public static class SimpleService { private static final IllegalStateException TEST_EXCEPTION = new IllegalStateException("Test exception"); - private AtomicLong counter = new AtomicLong(); + private final AtomicLong counter = new AtomicLong(); @CacheResult public Object get(long id) { diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java index 0c6671ebbb07..9b99bfb503a4 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,12 +35,12 @@ /** * @author Stephane Nicoll */ -public class JCacheInterceptorTests extends AbstractJCacheTests { +class JCacheInterceptorTests extends AbstractJCacheTests { private final CacheOperationInvoker dummyInvoker = new DummyInvoker(null); @Test - public void severalCachesNotSupported() { + void severalCachesNotSupported() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( cacheManager, new NamedCacheResolver(cacheManager, "default", "simpleCache"), defaultExceptionCacheResolver, defaultKeyGenerator)); @@ -54,7 +54,7 @@ cacheManager, new NamedCacheResolver(cacheManager, "default", "simpleCache"), } @Test - public void noCacheCouldBeResolved() { + void noCacheCouldBeResolved() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( cacheManager, new NamedCacheResolver(cacheManager), // Returns empty list defaultExceptionCacheResolver, defaultKeyGenerator)); @@ -67,18 +67,18 @@ cacheManager, new NamedCacheResolver(cacheManager), // Returns empty list } @Test - public void cacheManagerMandatoryIfCacheResolverNotSet() { + void cacheManagerMandatoryIfCacheResolverNotSet() { assertThatIllegalStateException().isThrownBy(() -> createOperationSource(null, null, null, defaultKeyGenerator)); } @Test - public void cacheManagerOptionalIfCacheResolversSet() { + void cacheManagerOptionalIfCacheResolversSet() { createOperationSource(null, defaultCacheResolver, defaultExceptionCacheResolver, defaultKeyGenerator); } @Test - public void cacheResultReturnsProperType() throws Throwable { + void cacheResultReturnsProperType() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( cacheManager, defaultCacheResolver, defaultExceptionCacheResolver, defaultKeyGenerator)); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java index 35db912df971..232a6b007624 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ /** * @author Stephane Nicoll */ -public class JCacheKeyGeneratorTests { +class JCacheKeyGeneratorTests { private TestKeyGenerator keyGenerator; @@ -53,7 +53,7 @@ public class JCacheKeyGeneratorTests { private Cache cache; @BeforeEach - public void setup() { + void setup() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); this.keyGenerator = context.getBean(TestKeyGenerator.class); this.simpleService = context.getBean(SimpleService.class); @@ -62,7 +62,7 @@ public void setup() { } @Test - public void getSimple() { + void getSimple() { this.keyGenerator.expect(1L); Object first = this.simpleService.get(1L); Object second = this.simpleService.get(1L); @@ -73,7 +73,7 @@ public void getSimple() { } @Test - public void getFlattenVararg() { + void getFlattenVararg() { this.keyGenerator.expect(1L, "foo", "bar"); Object first = this.simpleService.get(1L, "foo", "bar"); Object second = this.simpleService.get(1L, "foo", "bar"); @@ -84,7 +84,7 @@ public void getFlattenVararg() { } @Test - public void getFiltered() { + void getFiltered() { this.keyGenerator.expect(1L); Object first = this.simpleService.getFiltered(1L, "foo", "bar"); Object second = this.simpleService.getFiltered(1L, "foo", "bar"); @@ -120,7 +120,7 @@ public SimpleService simpleService() { @CacheDefaults(cacheName = "test") public static class SimpleService { - private AtomicLong counter = new AtomicLong(); + private final AtomicLong counter = new AtomicLong(); @CacheResult public Object get(long id) { diff --git a/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java index cf0487eb71cb..c5b03cf26bc1 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,18 +71,18 @@ void trackCacheName(TestInfo testInfo) { @Test - public void getOnExistingCache() { + void getOnExistingCache() { assertThat(getCacheManager(false).getCache(CACHE_NAME)).isInstanceOf(getCacheType()); } @Test - public void getOnNewCache() { + void getOnNewCache() { T cacheManager = getCacheManager(false); addNativeCache(this.cacheName); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + assertThat(cacheManager.getCacheNames()).doesNotContain(this.cacheName); try { assertThat(cacheManager.getCache(this.cacheName)).isInstanceOf(getCacheType()); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isTrue(); + assertThat(cacheManager.getCacheNames()).contains(this.cacheName); } finally { removeNativeCache(this.cacheName); @@ -90,27 +90,27 @@ public void getOnNewCache() { } @Test - public void getOnUnknownCache() { + void getOnUnknownCache() { T cacheManager = getCacheManager(false); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + assertThat(cacheManager.getCacheNames()).doesNotContain(this.cacheName); assertThat(cacheManager.getCache(this.cacheName)).isNull(); } @Test - public void getTransactionalOnExistingCache() { + void getTransactionalOnExistingCache() { assertThat(getCacheManager(true).getCache(CACHE_NAME)) .isInstanceOf(TransactionAwareCacheDecorator.class); } @Test - public void getTransactionalOnNewCache() { + void getTransactionalOnNewCache() { T cacheManager = getCacheManager(true); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + assertThat(cacheManager.getCacheNames()).doesNotContain(this.cacheName); addNativeCache(this.cacheName); try { assertThat(cacheManager.getCache(this.cacheName)) .isInstanceOf(TransactionAwareCacheDecorator.class); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isTrue(); + assertThat(cacheManager.getCacheNames()).contains(this.cacheName); } finally { removeNativeCache(this.cacheName); diff --git a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java index 66369edfe6c0..dd4adb1c0730 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,25 +30,25 @@ * @author Stephane Nicoll * @author Juergen Hoeller */ -public class TransactionAwareCacheDecoratorTests { +class TransactionAwareCacheDecoratorTests { private final TransactionTemplate txTemplate = new TransactionTemplate(new CallCountingTransactionManager()); @Test - public void createWithNullTarget() { + void createWithNullTarget() { assertThatIllegalArgumentException().isThrownBy(() -> new TransactionAwareCacheDecorator(null)); } @Test - public void getTargetCache() { + void getTargetCache() { Cache target = new ConcurrentMapCache("testCache"); TransactionAwareCacheDecorator cache = new TransactionAwareCacheDecorator(target); assertThat(cache.getTargetCache()).isSameAs(target); } @Test - public void regularOperationsOnTarget() { + void regularOperationsOnTarget() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); assertThat(cache.getName()).isEqualTo(target.getName()); @@ -64,7 +64,7 @@ public void regularOperationsOnTarget() { } @Test - public void putNonTransactional() { + void putNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); @@ -74,7 +74,7 @@ public void putNonTransactional() { } @Test - public void putTransactional() { + void putTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -88,7 +88,7 @@ public void putTransactional() { } @Test - public void putIfAbsentNonTransactional() { + void putIfAbsentNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); @@ -101,7 +101,7 @@ public void putIfAbsentNonTransactional() { } @Test - public void putIfAbsentTransactional() { // no transactional support for putIfAbsent + void putIfAbsentTransactional() { // no transactional support for putIfAbsent Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -118,7 +118,7 @@ public void putIfAbsentTransactional() { // no transactional support for putIfA } @Test - public void evictNonTransactional() { + void evictNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -129,7 +129,7 @@ public void evictNonTransactional() { } @Test - public void evictTransactional() { + void evictTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -144,7 +144,7 @@ public void evictTransactional() { } @Test - public void evictIfPresentNonTransactional() { + void evictIfPresentNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -155,7 +155,7 @@ public void evictIfPresentNonTransactional() { } @Test - public void evictIfPresentTransactional() { // no transactional support for evictIfPresent + void evictIfPresentTransactional() { // no transactional support for evictIfPresent Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -170,7 +170,7 @@ public void evictIfPresentTransactional() { // no transactional support for evi } @Test - public void clearNonTransactional() { + void clearNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -181,7 +181,7 @@ public void clearNonTransactional() { } @Test - public void clearTransactional() { + void clearTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -196,7 +196,7 @@ public void clearTransactional() { } @Test - public void invalidateNonTransactional() { + void invalidateNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -207,7 +207,7 @@ public void invalidateNonTransactional() { } @Test - public void invalidateTransactional() { // no transactional support for invalidate + void invalidateTransactional() { // no transactional support for invalidate Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); diff --git a/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java index 5312152ce4ce..7fd2cfe272bb 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * @author Chris Beams * @since 10.09.2003 */ -public class SimpleMailMessageTests { +class SimpleMailMessageTests { @Test - public void testSimpleMessageCopyCtor() { + void testSimpleMessageCopyCtor() { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom("me@mail.org"); message.setTo("you@mail.org"); @@ -45,8 +45,8 @@ public void testSimpleMessageCopyCtor() { assertThat(messageCopy.getTo()[0]).isEqualTo("you@mail.org"); message.setReplyTo("reply@mail.org"); - message.setCc(new String[]{"he@mail.org", "she@mail.org"}); - message.setBcc(new String[]{"us@mail.org", "them@mail.org"}); + message.setCc("he@mail.org", "she@mail.org"); + message.setBcc("us@mail.org", "them@mail.org"); Date sentDate = new Date(); message.setSentDate(sentDate); message.setSubject("my subject"); @@ -56,11 +56,11 @@ public void testSimpleMessageCopyCtor() { assertThat(message.getReplyTo()).isEqualTo("reply@mail.org"); assertThat(message.getTo()[0]).isEqualTo("you@mail.org"); List ccs = Arrays.asList(message.getCc()); - assertThat(ccs.contains("he@mail.org")).isTrue(); - assertThat(ccs.contains("she@mail.org")).isTrue(); + assertThat(ccs).contains("he@mail.org"); + assertThat(ccs).contains("she@mail.org"); List bccs = Arrays.asList(message.getBcc()); - assertThat(bccs.contains("us@mail.org")).isTrue(); - assertThat(bccs.contains("them@mail.org")).isTrue(); + assertThat(bccs).contains("us@mail.org"); + assertThat(bccs).contains("them@mail.org"); assertThat(message.getSentDate()).isEqualTo(sentDate); assertThat(message.getSubject()).isEqualTo("my subject"); assertThat(message.getText()).isEqualTo("my text"); @@ -70,23 +70,23 @@ public void testSimpleMessageCopyCtor() { assertThat(messageCopy.getReplyTo()).isEqualTo("reply@mail.org"); assertThat(messageCopy.getTo()[0]).isEqualTo("you@mail.org"); ccs = Arrays.asList(messageCopy.getCc()); - assertThat(ccs.contains("he@mail.org")).isTrue(); - assertThat(ccs.contains("she@mail.org")).isTrue(); + assertThat(ccs).contains("he@mail.org"); + assertThat(ccs).contains("she@mail.org"); bccs = Arrays.asList(message.getBcc()); - assertThat(bccs.contains("us@mail.org")).isTrue(); - assertThat(bccs.contains("them@mail.org")).isTrue(); + assertThat(bccs).contains("us@mail.org"); + assertThat(bccs).contains("them@mail.org"); assertThat(messageCopy.getSentDate()).isEqualTo(sentDate); assertThat(messageCopy.getSubject()).isEqualTo("my subject"); assertThat(messageCopy.getText()).isEqualTo("my text"); } @Test - public void testDeepCopyOfStringArrayTypedFieldsOnCopyCtor() throws Exception { + void testDeepCopyOfStringArrayTypedFieldsOnCopyCtor() { SimpleMailMessage original = new SimpleMailMessage(); - original.setTo(new String[]{"fiona@mail.org", "apple@mail.org"}); - original.setCc(new String[]{"he@mail.org", "she@mail.org"}); - original.setBcc(new String[]{"us@mail.org", "them@mail.org"}); + original.setTo("fiona@mail.org", "apple@mail.org"); + original.setCc("he@mail.org", "she@mail.org"); + original.setBcc("us@mail.org", "them@mail.org"); SimpleMailMessage copy = new SimpleMailMessage(original); @@ -121,6 +121,7 @@ public final void testHashCode() { assertThat(message2.hashCode()).isEqualTo(message1.hashCode()); } + @Test public final void testEqualsObject() { SimpleMailMessage message1; SimpleMailMessage message2; @@ -128,7 +129,7 @@ public final void testEqualsObject() { // Same object is equal message1 = new SimpleMailMessage(); message2 = message1; - assertThat(message1.equals(message2)).isTrue(); + assertThat(message1).isEqualTo(message2); // Null object is not equal message1 = new SimpleMailMessage(); @@ -143,7 +144,7 @@ public final void testEqualsObject() { // Equal values are equal message1 = new SimpleMailMessage(); message2 = new SimpleMailMessage(); - assertThat(message1.equals(message2)).isTrue(); + assertThat(message1).isEqualTo(message2); message1 = new SimpleMailMessage(); message1.setFrom("from@somewhere"); @@ -155,17 +156,17 @@ public final void testEqualsObject() { message1.setSubject("subject"); message1.setText("text"); message2 = new SimpleMailMessage(message1); - assertThat(message1.equals(message2)).isTrue(); + assertThat(message1).isEqualTo(message2); } @Test - public void testCopyCtorChokesOnNullOriginalMessage() throws Exception { + void testCopyCtorChokesOnNullOriginalMessage() { assertThatIllegalArgumentException().isThrownBy(() -> new SimpleMailMessage(null)); } @Test - public void testCopyToChokesOnNullTargetMessage() throws Exception { + void testCopyToChokesOnNullTargetMessage() { assertThatIllegalArgumentException().isThrownBy(() -> new SimpleMailMessage().copyTo(null)); } diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java index ebbf493cd5f0..143856494189 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,10 +29,10 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class ConfigurableMimeFileTypeMapTests { +class ConfigurableMimeFileTypeMapTests { @Test - public void againstDefaultConfiguration() throws Exception { + void againstDefaultConfiguration() { ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); ftm.afterPropertiesSet(); @@ -45,15 +45,15 @@ public void againstDefaultConfiguration() throws Exception { } @Test - public void againstDefaultConfigurationWithFilePath() throws Exception { + void againstDefaultConfigurationWithFilePath() { ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); assertThat(ftm.getContentType(new File("/tmp/foobar.HTM"))).as("Invalid content type for HTM").isEqualTo("text/html"); } @Test - public void withAdditionalMappings() throws Exception { + void withAdditionalMappings() { ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); - ftm.setMappings(new String[] {"foo/bar HTM foo", "foo/cpp c++"}); + ftm.setMappings("foo/bar HTM foo", "foo/cpp c++"); ftm.afterPropertiesSet(); assertThat(ftm.getContentType("foobar.HTM")).as("Invalid content type for HTM - override didn't work").isEqualTo("foo/bar"); @@ -62,7 +62,7 @@ public void withAdditionalMappings() throws Exception { } @Test - public void withCustomMappingLocation() throws Exception { + void withCustomMappingLocation() { Resource resource = new ClassPathResource("test.mime.types", getClass()); ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java index cb321a09ecbd..35d062615594 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * @author Sam Brannen * @since 09.07.2005 */ -public class InternetAddressEditorTests { +class InternetAddressEditorTests { private static final String EMPTY = ""; private static final String SIMPLE = "nobody@nowhere.com"; @@ -36,42 +36,42 @@ public class InternetAddressEditorTests { @Test - public void uninitialized() { + void uninitialized() { assertThat(editor.getAsText()).as("Uninitialized editor did not return empty value string").isEmpty(); } @Test - public void setNull() { + void setNull() { editor.setAsText(null); assertThat(editor.getAsText()).as("Setting null did not result in empty value string").isEmpty(); } @Test - public void setEmpty() { + void setEmpty() { editor.setAsText(EMPTY); assertThat(editor.getAsText()).as("Setting empty string did not result in empty value string").isEmpty(); } @Test - public void allWhitespace() { + void allWhitespace() { editor.setAsText(" "); assertThat(editor.getAsText()).as("All whitespace was not recognized").isEmpty(); } @Test - public void simpleGoodAddress() { + void simpleGoodAddress() { editor.setAsText(SIMPLE); assertThat(editor.getAsText()).as("Simple email address failed").isEqualTo(SIMPLE); } @Test - public void excessWhitespace() { + void excessWhitespace() { editor.setAsText(" " + SIMPLE + " "); assertThat(editor.getAsText()).as("Whitespace was not stripped").isEqualTo(SIMPLE); } @Test - public void simpleBadAddress() { + void simpleBadAddress() { assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText(BAD)); } diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java index 8daf7d1b35c6..2007758b43ea 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; @@ -72,7 +73,7 @@ void javaMailSenderWithSimpleMessage() throws Exception { simpleMessage.setTo("you@mail.org"); simpleMessage.setCc("he@mail.org", "she@mail.org"); simpleMessage.setBcc("us@mail.org", "them@mail.org"); - Date sentDate = new GregorianCalendar(2004, 1, 1).getTime(); + Date sentDate = new GregorianCalendar(2004, Calendar.FEBRUARY, 1).getTime(); simpleMessage.setSentDate(sentDate); simpleMessage.setSubject("my subject"); simpleMessage.setText("my text"); @@ -305,7 +306,7 @@ protected Transport getTransport(Session sess) throws NoSuchProviderException { MimeMessage mimeMessage = sender.createMimeMessage(); mimeMessage.setSubject("custom"); mimeMessage.setRecipient(RecipientType.TO, new InternetAddress("you@mail.org")); - mimeMessage.setSentDate(new GregorianCalendar(2005, 3, 1).getTime()); + mimeMessage.setSentDate(new GregorianCalendar(2005, Calendar.APRIL, 1).getTime()); sender.send(mimeMessage); assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); @@ -386,9 +387,9 @@ void failedSimpleMessage() throws Exception { assertThat(sender.transport.isCloseCalled()).isTrue(); assertThat(sender.transport.getSentMessages()).hasSize(1); assertThat(sender.transport.getSentMessage(0).getAllRecipients()[0]).isEqualTo(new InternetAddress("she@mail.org")); - assertThat(ex.getFailedMessages().keySet()).containsExactly(simpleMessage1); - Exception subEx = ex.getFailedMessages().values().iterator().next(); - assertThat(subEx).isInstanceOf(MessagingException.class).hasMessage("failed"); + assertThat(ex.getFailedMessages()).containsOnlyKeys(simpleMessage1); + assertThat(ex.getFailedMessages().get(simpleMessage1)) + .isInstanceOf(MessagingException.class).hasMessage("failed"); } } @@ -413,9 +414,9 @@ void failedMimeMessage() throws Exception { assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); assertThat(sender.transport.isCloseCalled()).isTrue(); assertThat(sender.transport.getSentMessages()).containsExactly(mimeMessage2); - assertThat(ex.getFailedMessages().keySet()).containsExactly(mimeMessage1); - Exception subEx = ex.getFailedMessages().values().iterator().next(); - assertThat(subEx).isInstanceOf(MessagingException.class).hasMessage("failed"); + assertThat(ex.getFailedMessages()).containsOnlyKeys(mimeMessage1); + assertThat(ex.getFailedMessages().get(mimeMessage1)) + .isInstanceOf(MessagingException.class).hasMessage("failed"); } } @@ -456,7 +457,7 @@ private static class MockTransport extends Transport { private String connectedUsername = null; private String connectedPassword = null; private boolean closeCalled = false; - private List sentMessages = new ArrayList<>(); + private final List sentMessages = new ArrayList<>(); private MockTransport(Session session, URLName urlName) { super(session, urlName); @@ -504,7 +505,7 @@ public void connect(String host, int port, String username, String password) thr @Override public synchronized void close() throws MessagingException { - if ("".equals(connectedHost)) { + if (this.connectedHost.isEmpty()) { throw new MessagingException("close failure"); } this.closeCalled = true; @@ -523,7 +524,7 @@ public void sendMessage(Message message, Address[] addresses) throws MessagingEx throw new MessagingException("No sentDate specified"); } if (message.getSubject() != null && message.getSubject().contains("custom")) { - assertThat(message.getSentDate()).isEqualTo(new GregorianCalendar(2005, 3, 1).getTime()); + assertThat(message.getSentDate()).isEqualTo(new GregorianCalendar(2005, Calendar.APRIL, 1).getTime()); } this.sentMessages.add(message); } diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/CronTriggerFactoryBeanTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/CronTriggerFactoryBeanTests.java index 79dc04418b68..1bc8cece861d 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/CronTriggerFactoryBeanTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/CronTriggerFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,35 @@ package org.springframework.scheduling.quartz; +import java.lang.reflect.Field; import java.text.ParseException; +import java.util.Arrays; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.quartz.CronTrigger; +import org.springframework.util.ReflectionUtils; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.quartz.Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY; +import static org.quartz.Trigger.MISFIRE_INSTRUCTION_SMART_POLICY; /** + * Tests for {@link CronTriggerFactoryBean}. + * * @author Stephane Nicoll + * @author Sam Brannen */ -public class CronTriggerFactoryBeanTests { +class CronTriggerFactoryBeanTests { + + private final CronTriggerFactoryBean factory = new CronTriggerFactoryBean(); + @Test - public void createWithoutJobDetail() throws ParseException { - CronTriggerFactoryBean factory = new CronTriggerFactoryBean(); + void createWithoutJobDetail() throws ParseException { factory.setName("myTrigger"); factory.setCronExpression("0 15 10 ? * *"); factory.afterPropertiesSet(); @@ -38,4 +52,39 @@ public void createWithoutJobDetail() throws ParseException { assertThat(trigger.getCronExpression()).isEqualTo("0 15 10 ? * *"); } + @Test + void setMisfireInstructionNameToUnsupportedValues() { + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(null)); + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(" ")); + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName("bogus")); + } + + /** + * This test effectively verifies that the internal 'constants' map is properly + * configured for all MISFIRE_INSTRUCTION_ constants defined in {@link CronTrigger}. + */ + @Test + void setMisfireInstructionNameToAllSupportedValues() { + streamMisfireInstructionConstants() + .map(Field::getName) + .forEach(name -> assertThatNoException().as(name).isThrownBy(() -> factory.setMisfireInstructionName(name))); + } + + @Test + void setMisfireInstruction() { + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstruction(999)); + + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_SMART_POLICY)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING)); + } + + + private static Stream streamMisfireInstructionConstants() { + return Arrays.stream(CronTrigger.class.getFields()) + .filter(ReflectionUtils::isPublicStaticFinal) + .filter(field -> field.getName().startsWith("MISFIRE_INSTRUCTION_")); + } + } diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java index 31426b1f6be5..7d1af052ae78 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ * @author Mark Fisher * @since 3.0 */ -public class QuartzSchedulerLifecycleTests { +class QuartzSchedulerLifecycleTests { @Test // SPR-6354 public void destroyLazyInitSchedulerWithDefaultShutdownOrderDoesNotHang() { diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index 35ea29c98e67..87d60ab0a009 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test; import org.quartz.Job; import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; import org.quartz.Scheduler; import org.quartz.SchedulerContext; import org.quartz.SchedulerFactory; @@ -352,7 +351,6 @@ void schedulerAccessorBean() throws Exception { } @Test - @SuppressWarnings("resource") void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("scheduler", new RootBeanDefinition(SchedulerFactoryBean.class)); @@ -363,7 +361,6 @@ void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { } @Test - @SuppressWarnings("resource") void schedulerAutoStartupFalse() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class) @@ -376,7 +373,7 @@ void schedulerAutoStartupFalse() throws Exception { } @Test - void schedulerRepositoryExposure() throws Exception { + void schedulerRepositoryExposure() { try (ClassPathXmlApplicationContext ctx = context("schedulerRepositoryExposure.xml")) { assertThat(ctx.getBean("scheduler")).isSameAs(SchedulerRepository.getInstance().lookup("myScheduler")); } @@ -387,23 +384,17 @@ void schedulerRepositoryExposure() throws Exception { * TODO: Against Quartz 2.2, this test's job doesn't actually execute anymore... */ @Test - void schedulerWithHsqlDataSource() throws Exception { + void schedulerWithHsqlDataSource() { DummyJob.param = 0; DummyJob.count = 0; try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) { JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class)); assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse(); - - /* - Thread.sleep(3000); - assertTrue("DummyJob should have been executed at least once.", DummyJob.count > 0); - */ } } @Test - @SuppressWarnings("resource") void schedulerFactoryBeanWithCustomJobStore() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); @@ -459,7 +450,7 @@ public void setParam(int value) { } @Override - public synchronized void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + public synchronized void execute(JobExecutionContext jobExecutionContext) { count++; } } @@ -480,7 +471,7 @@ public void setParam(int value) { } @Override - protected synchronized void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { + protected synchronized void executeInternal(JobExecutionContext jobExecutionContext) { count++; } } diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java index 7e422ed9b331..0f3fd26dd967 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ * * @author Sebastien Deleuze */ -public class SchedulerFactoryBeanRuntimeHintsTests { +class SchedulerFactoryBeanRuntimeHintsTests { private final RuntimeHints hints = new RuntimeHints(); diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBeanTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBeanTests.java index 6db0bcc82d2d..f3ceba0b26bd 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBeanTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,39 @@ package org.springframework.scheduling.quartz; -import java.text.ParseException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.quartz.SimpleTrigger; +import org.springframework.util.ReflectionUtils; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.quartz.SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW; +import static org.quartz.SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT; +import static org.quartz.SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT; +import static org.quartz.SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT; +import static org.quartz.SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT; +import static org.quartz.Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY; +import static org.quartz.Trigger.MISFIRE_INSTRUCTION_SMART_POLICY; /** + * Tests for {@link SimpleTriggerFactoryBean}. + * * @author Stephane Nicoll + * @author Sam Brannen */ -public class SimpleTriggerFactoryBeanTests { +class SimpleTriggerFactoryBeanTests { + + private final SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean(); + @Test - public void createWithoutJobDetail() throws ParseException { - SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean(); + void createWithoutJobDetail() { factory.setName("myTrigger"); factory.setRepeatCount(5); factory.setRepeatInterval(1000L); @@ -40,4 +58,42 @@ public void createWithoutJobDetail() throws ParseException { assertThat(trigger.getRepeatInterval()).isEqualTo(1000L); } + @Test + void setMisfireInstructionNameToUnsupportedValues() { + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(null)); + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(" ")); + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName("bogus")); + } + + /** + * This test effectively verifies that the internal 'constants' map is properly + * configured for all MISFIRE_INSTRUCTION_ constants defined in {@link SimpleTrigger}. + */ + @Test + void setMisfireInstructionNameToAllSupportedValues() { + streamMisfireInstructionConstants() + .map(Field::getName) + .forEach(name -> assertThatNoException().as(name).isThrownBy(() -> factory.setMisfireInstructionName(name))); + } + + @Test + void setMisfireInstruction() { + assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstruction(999)); + + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_SMART_POLICY)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_FIRE_NOW)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT)); + assertThatNoException().isThrownBy(() -> factory.setMisfireInstruction(MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT)); + } + + + private static Stream streamMisfireInstructionConstants() { + return Arrays.stream(SimpleTrigger.class.getFields()) + .filter(ReflectionUtils::isPublicStaticFinal) + .filter(field -> field.getName().startsWith("MISFIRE_INSTRUCTION_")); + } + } diff --git a/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java b/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java index c0a07e448a0c..0ab2045390fd 100644 --- a/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java +++ b/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,12 +39,12 @@ * @author Issam El-atif * @author Sam Brannen */ -public class FreeMarkerConfigurationFactoryBeanTests { +class FreeMarkerConfigurationFactoryBeanTests { private final FreeMarkerConfigurationFactoryBean fcfb = new FreeMarkerConfigurationFactoryBean(); @Test - public void freeMarkerConfigurationFactoryBeanWithConfigLocation() throws Exception { + void freeMarkerConfigurationFactoryBeanWithConfigLocation() { fcfb.setConfigLocation(new FileSystemResource("myprops.properties")); Properties props = new Properties(); props.setProperty("myprop", "/mydir"); @@ -53,7 +53,7 @@ public void freeMarkerConfigurationFactoryBeanWithConfigLocation() throws Except } @Test - public void freeMarkerConfigurationFactoryBeanWithResourceLoaderPath() throws Exception { + void freeMarkerConfigurationFactoryBeanWithResourceLoaderPath() throws Exception { fcfb.setTemplateLoaderPath("file:/mydir"); fcfb.afterPropertiesSet(); Configuration cfg = fcfb.getObject(); diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java index b1fc5cda8bff..b8892b536cf3 100644 --- a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public abstract class AbstractJCacheAnnotationTests { protected abstract ApplicationContext getApplicationContext(); @BeforeEach - public void setUp(TestInfo testInfo) { + protected void setUp(TestInfo testInfo) { this.keyItem = testInfo.getTestMethod().get().getName(); this.ctx = getApplicationContext(); this.service = this.ctx.getBean(JCacheableService.class); @@ -62,14 +62,14 @@ public void setUp(TestInfo testInfo) { } @Test - public void cache() { + protected void cache() { Object first = service.cache(this.keyItem); Object second = service.cache(this.keyItem); assertThat(second).isSameAs(first); } @Test - public void cacheNull() { + protected void cacheNull() { Cache cache = getCache(DEFAULT_CACHE); assertThat(cache.get(this.keyItem)).isNull(); @@ -85,7 +85,7 @@ public void cacheNull() { } @Test - public void cacheException() { + protected void cacheException() { Cache cache = getCache(EXCEPTION_CACHE); Object key = createKey(this.keyItem); @@ -100,7 +100,7 @@ public void cacheException() { } @Test - public void cacheExceptionVetoed() { + protected void cacheExceptionVetoed() { Cache cache = getCache(EXCEPTION_CACHE); Object key = createKey(this.keyItem); @@ -112,7 +112,7 @@ public void cacheExceptionVetoed() { } @Test - public void cacheCheckedException() { + protected void cacheCheckedException() { Cache cache = getCache(EXCEPTION_CACHE); Object key = createKey(this.keyItem); @@ -128,7 +128,7 @@ public void cacheCheckedException() { @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test - public void cacheExceptionRewriteCallStack() { + protected void cacheExceptionRewriteCallStack() { long ref = service.exceptionInvocations(); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> service.cacheWithException(this.keyItem, true)) @@ -151,14 +151,14 @@ public void cacheExceptionRewriteCallStack() { } @Test - public void cacheAlwaysInvoke() { + protected void cacheAlwaysInvoke() { Object first = service.cacheAlwaysInvoke(this.keyItem); Object second = service.cacheAlwaysInvoke(this.keyItem); assertThat(second).isNotSameAs(first); } @Test - public void cacheWithPartialKey() { + protected void cacheWithPartialKey() { Object first = service.cacheWithPartialKey(this.keyItem, true); Object second = service.cacheWithPartialKey(this.keyItem, false); // second argument not used, see config @@ -166,7 +166,7 @@ public void cacheWithPartialKey() { } @Test - public void cacheWithCustomCacheResolver() { + protected void cacheWithCustomCacheResolver() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -177,7 +177,7 @@ public void cacheWithCustomCacheResolver() { } @Test - public void cacheWithCustomKeyGenerator() { + protected void cacheWithCustomKeyGenerator() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -187,7 +187,7 @@ public void cacheWithCustomKeyGenerator() { } @Test - public void put() { + protected void put() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -202,7 +202,7 @@ public void put() { } @Test - public void putWithException() { + protected void putWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -218,7 +218,7 @@ public void putWithException() { } @Test - public void putWithExceptionVetoPut() { + protected void putWithExceptionVetoPut() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -231,7 +231,7 @@ public void putWithExceptionVetoPut() { } @Test - public void earlyPut() { + protected void earlyPut() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -246,7 +246,7 @@ public void earlyPut() { } @Test - public void earlyPutWithException() { + protected void earlyPutWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -262,7 +262,7 @@ public void earlyPutWithException() { } @Test - public void earlyPutWithExceptionVetoPut() { + protected void earlyPutWithExceptionVetoPut() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -277,7 +277,7 @@ public void earlyPutWithExceptionVetoPut() { } @Test - public void remove() { + protected void remove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -290,7 +290,7 @@ public void remove() { } @Test - public void removeWithException() { + protected void removeWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -304,7 +304,7 @@ public void removeWithException() { } @Test - public void removeWithExceptionVetoRemove() { + protected void removeWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -319,7 +319,7 @@ public void removeWithExceptionVetoRemove() { } @Test - public void earlyRemove() { + protected void earlyRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -332,7 +332,7 @@ public void earlyRemove() { } @Test - public void earlyRemoveWithException() { + protected void earlyRemoveWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -345,7 +345,7 @@ public void earlyRemoveWithException() { } @Test - public void earlyRemoveWithExceptionVetoRemove() { + protected void earlyRemoveWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -359,7 +359,7 @@ public void earlyRemoveWithExceptionVetoRemove() { } @Test - public void removeAll() { + protected void removeAll() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -371,7 +371,7 @@ public void removeAll() { } @Test - public void removeAllWithException() { + protected void removeAllWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -384,7 +384,7 @@ public void removeAllWithException() { } @Test - public void removeAllWithExceptionVetoRemove() { + protected void removeAllWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -396,7 +396,7 @@ public void removeAllWithExceptionVetoRemove() { } @Test - public void earlyRemoveAll() { + protected void earlyRemoveAll() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -408,7 +408,7 @@ public void earlyRemoveAll() { } @Test - public void earlyRemoveAllWithException() { + protected void earlyRemoveAllWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -420,7 +420,7 @@ public void earlyRemoveAllWithException() { } @Test - public void earlyRemoveAllWithExceptionVetoRemove() { + protected void earlyRemoveAllWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index 49bd23ea22d2..0256d6bfdbfb 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -11,7 +11,9 @@ dependencies { api(project(":spring-beans")) api(project(":spring-core")) api(project(":spring-expression")) + api("io.micrometer:micrometer-observation") optional(project(":spring-instrument")) + optional("io.projectreactor:reactor-core") optional("jakarta.annotation:jakarta.annotation-api") optional("jakarta.ejb:jakarta.ejb-api") optional("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api") @@ -19,29 +21,36 @@ dependencies { optional("jakarta.interceptor:jakarta.interceptor-api") optional("jakarta.validation:jakarta.validation-api") optional("javax.annotation:javax.annotation-api") + optional("javax.inject:javax.inject") optional("javax.money:money-api") optional("org.apache.groovy:groovy") optional("org.apache-extras.beanshell:bsh") optional("org.aspectj:aspectjweaver") + optional("org.crac:crac") optional("org.hibernate:hibernate-validator") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") + optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") optional("org.reactivestreams:reactive-streams") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation(testFixtures(project(":spring-beans"))) testFixturesImplementation("com.google.code.findbugs:jsr305") + testFixturesImplementation("io.projectreactor:reactor-test") testFixturesImplementation("org.assertj:assertj-core") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-aop"))) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-core"))) - testImplementation("io.projectreactor:reactor-core") testImplementation("jakarta.inject:jakarta.inject-tck") testImplementation("org.apache.commons:commons-pool2") testImplementation("org.apache.groovy:groovy-jsr223") testImplementation("org.apache.groovy:groovy-xml") testImplementation("org.awaitility:awaitility") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + testImplementation("io.reactivex.rxjava3:rxjava") + testImplementation('io.micrometer:context-propagation') + testImplementation("io.micrometer:micrometer-observation-test") testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api") testRuntimeOnly("org.glassfish:jakarta.el") // Substitute for javax.management:jmxremote_optional:1.0.1_04 (not available on Maven Central) diff --git a/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java b/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java index 51a568ca30a1..8fb9581b1e00 100644 --- a/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java +++ b/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,8 +57,8 @@ public void setup() { @TearDown public void teardown() { - System.getProperties().remove("country"); - System.getProperties().remove("name"); + System.clearProperty("country"); + System.clearProperty("name"); } } diff --git a/spring-context/src/main/java/org/springframework/cache/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java index 648ff88e3957..3df983a9a268 100644 --- a/spring-context/src/main/java/org/springframework/cache/Cache.java +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -17,15 +17,17 @@ package org.springframework.cache; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; import org.springframework.lang.Nullable; /** * Interface that defines common cache operations. * - *

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

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

Note: Due to the generic use of caching, it is recommended * that implementations allow storage of {@code null} values @@ -106,6 +108,66 @@ public interface Cache { @Nullable T get(Object key, Callable valueLoader); + /** + * Return the value to which this cache maps the specified key, + * wrapped in a {@link CompletableFuture}. This operation must not block + * but is allowed to return a completed {@link CompletableFuture} if the + * corresponding value is immediately available. + *

Can return {@code null} if the cache can immediately determine that + * it contains no mapping for this key (e.g. through an in-memory key map). + * Otherwise, the cached value will be returned in the {@link CompletableFuture}, + * with {@code null} indicating a late-determined cache miss. A nested + * {@link ValueWrapper} potentially indicates a nullable cached value; + * the cached value may also be represented as a plain element if null + * values are not supported. Calling code needs to be prepared to handle + * all those variants of the result returned by this method. + * @param key the key whose associated value is to be returned + * @return the value to which this cache maps the specified key, contained + * within a {@link CompletableFuture} which may also be empty when a cache + * miss has been late-determined. A straight {@code null} being returned + * means that the cache immediately determined that it contains no mapping + * for this key. A {@link ValueWrapper} contained within the + * {@code CompletableFuture} indicates a cached value that is potentially + * {@code null}; this is sensible in a late-determined scenario where a regular + * CompletableFuture-contained {@code null} indicates a cache miss. However, + * a cache may also return a plain value if it does not support the actual + * caching of {@code null} values, avoiding the extra level of value wrapping. + * Spring's cache processing can deal with all such implementation strategies. + * @since 6.1 + * @see #retrieve(Object, Supplier) + */ + @Nullable + default CompletableFuture retrieve(Object key) { + throw new UnsupportedOperationException( + getClass().getName() + " does not support CompletableFuture-based retrieval"); + } + + /** + * Return the value to which this cache maps the specified key, obtaining + * that value from {@code valueLoader} if necessary. This method provides + * a simple substitute for the conventional "if cached, return; otherwise + * create, cache and return" pattern, based on {@link CompletableFuture}. + * This operation must not block. + *

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

Null values always indicate a user-level {@code null} value with this + * method. The provided {@link CompletableFuture} handle produces a value + * or raises an exception. If the {@code valueLoader} raises an exception, + * it will be propagated to the returned {@code CompletableFuture} handle. + * @param key the key whose associated value is to be returned + * @return the value to which this cache maps the specified key, contained + * within a {@link CompletableFuture} which will never be {@code null}. + * The provided future is expected to produce a value or raise an exception. + * @since 6.1 + * @see #retrieve(Object) + * @see #get(Object, Callable) + */ + default CompletableFuture retrieve(Object key, Supplier> valueLoader) { + throw new UnsupportedOperationException( + getClass().getName() + " does not support CompletableFuture-based retrieval"); + } + /** * Associate the specified value with the specified key in this cache. *

If the cache previously contained a mapping for this key, the old @@ -114,6 +176,11 @@ public interface Cache { * fashion, with subsequent lookups possibly not seeing the entry yet. * This may for example be the case with transactional cache decorators. * Use {@link #putIfAbsent} for guaranteed immediate registration. + *

If the cache is supposed to be compatible with {@link CompletableFuture} + * and reactive interactions, the put operation needs to be effectively + * non-blocking, with any backend write-through happening asynchronously. + * This goes along with a cache implemented and configured to support + * {@link #retrieve(Object)} and {@link #retrieve(Object, Supplier)}. * @param key the key with which the specified value is to be associated * @param value the value to be associated with the specified key * @see #putIfAbsent(Object, Object) @@ -162,6 +229,11 @@ default ValueWrapper putIfAbsent(Object key, @Nullable Object value) { * fashion, with subsequent lookups possibly still seeing the entry. * This may for example be the case with transactional cache decorators. * Use {@link #evictIfPresent} for guaranteed immediate removal. + *

If the cache is supposed to be compatible with {@link CompletableFuture} + * and reactive interactions, the evict operation needs to be effectively + * non-blocking, with any backend write-through happening asynchronously. + * This goes along with a cache implemented and configured to support + * {@link #retrieve(Object)} and {@link #retrieve(Object, Supplier)}. * @param key the key whose mapping is to be removed from the cache * @see #evictIfPresent(Object) */ @@ -194,6 +266,11 @@ default boolean evictIfPresent(Object key) { * fashion, with subsequent lookups possibly still seeing the entries. * This may for example be the case with transactional cache decorators. * Use {@link #invalidate()} for guaranteed immediate removal of entries. + *

If the cache is supposed to be compatible with {@link CompletableFuture} + * and reactive interactions, the clear operation needs to be effectively + * non-blocking, with any backend write-through happening asynchronously. + * This goes along with a cache implemented and configured to support + * {@link #retrieve(Object)} and {@link #retrieve(Object, Supplier)}. * @see #invalidate() */ void clear(); @@ -238,7 +315,7 @@ class ValueRetrievalException extends RuntimeException { @Nullable private final Object key; - public ValueRetrievalException(@Nullable Object key, Callable loader, Throwable ex) { + public ValueRetrievalException(@Nullable Object key, Callable loader, @Nullable Throwable ex) { super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); this.key = key; } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java index 234f353b142d..78da3a22e5ab 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ * @author Stephane Nicoll * @author Sam Brannen * @since 4.1 + * @see Cacheable */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -42,8 +43,10 @@ * Names of the default caches to consider for caching operations defined * in the annotated class. *

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

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

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

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

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

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

Note that asynchronous/reactive cache access may not fully consult all + * specified caches, depending on the target cache. In the case of late-determined + * cache misses (e.g. with Redis), further caches will not get consulted anymore. + * As a consequence, specifying multiple cache names in an async cache mode setup + * only makes sense with early-determined cache misses (e.g. with Caffeine). * @since 4.2 * @see #value * @see CacheConfig#cacheNames diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java index fe26f7d56d93..85c372dfeb3c 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,8 @@ * cache management. * *

See @{@link EnableCaching} for general examples and context; see - * {@link #cacheManager()}, {@link #cacheResolver()} and {@link #keyGenerator()} - * for detailed instructions. + * {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, and + * {@link #errorHandler()} for detailed instructions. * * @author Chris Beams * @author Stephane Nicoll @@ -46,14 +46,15 @@ public interface CachingConfigurer { * management of the cache resolution, consider setting the * {@link CacheResolver} directly. *

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

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

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

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

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

 	 * @Configuration
 	 * @EnableCaching
-	 * public class AppConfig implements CachingConfigurer {
+	 * class AppConfig implements CachingConfigurer {
 	 *     @Bean // important!
 	 *     @Override
-	 *     public CacheResolver cacheResolver() {
+	 *     CacheResolver cacheResolver() {
 	 *         // configure and return CacheResolver instance
 	 *     }
 	 *     // ...
@@ -95,20 +97,8 @@ default CacheResolver cacheResolver() {
 
 	/**
 	 * Return the key generator bean to use for annotation-driven cache management.
-	 * Implementations must explicitly declare
-	 * {@link org.springframework.context.annotation.Bean @Bean}, e.g.
-	 * 
-	 * @Configuration
-	 * @EnableCaching
-	 * public class AppConfig implements CachingConfigurer {
-	 *     @Bean // important!
-	 *     @Override
-	 *     public KeyGenerator keyGenerator() {
-	 *         // configure and return KeyGenerator instance
-	 *     }
-	 *     // ...
-	 * }
-	 * 
+ *

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

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

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

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

By default, {@link org.springframework.cache.interceptor.SimpleCacheErrorHandler} + * is used, which throws the exception back at the client. * See @{@link EnableCaching} for more complete examples. */ @Nullable diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java index 06ea231402d4..75ad63f82747 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,16 +35,16 @@ *

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

{@link CachingConfigurer} offers additional customization options: * see the {@link CachingConfigurer} javadoc for further details. diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java index 1a17605578a1..0facea830e29 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,11 @@ package org.springframework.cache.concurrent; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Supplier; import org.springframework.cache.support.AbstractValueAdaptingCache; import org.springframework.core.serializer.support.SerializationDelegate; @@ -26,13 +29,17 @@ import org.springframework.util.Assert; /** - * Simple {@link org.springframework.cache.Cache} implementation based on the - * core JDK {@code java.util.concurrent} package. + * Simple {@link org.springframework.cache.Cache} implementation based on the core + * JDK {@code java.util.concurrent} package. * *

Useful for testing or simple caching scenarios, typically in combination * with {@link org.springframework.cache.support.SimpleCacheManager} or * dynamically through {@link ConcurrentMapCacheManager}. * + *

Supports the {@link #retrieve(Object)} and {@link #retrieve(Object, Supplier)} + * operations in a best-effort fashion, relying on default {@link CompletableFuture} + * execution (typically within the JVM's {@link ForkJoinPool#commonPool()}). + * *

Note: As {@link ConcurrentHashMap} (the default implementation used) * does not allow for {@code null} values to be stored, this class will replace * them with a predefined internal object. This behavior can be changed through the @@ -149,6 +156,21 @@ public T get(Object key, Callable valueLoader) { })); } + @Override + @Nullable + public CompletableFuture retrieve(Object key) { + Object value = lookup(key); + return (value != null ? CompletableFuture.completedFuture( + isAllowNullValues() ? toValueWrapper(value) : fromStoreValue(value)) : null); + } + + @SuppressWarnings("unchecked") + @Override + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + return CompletableFuture.supplyAsync(() -> + (T) fromStoreValue(this.store.computeIfAbsent(key, k -> toStoreValue(valueLoader.get().join())))); + } + @Override public void put(Object key, @Nullable Object value) { this.store.put(key, toStoreValue(value)); @@ -201,6 +223,7 @@ protected Object toStoreValue(@Nullable Object userValue) { } @Override + @Nullable protected Object fromStoreValue(@Nullable Object storeValue) { if (storeValue != null && this.serialization != null) { try { diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java index 2d993db5ba66..585d8b2059f4 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.cache.Cache; @@ -35,11 +36,15 @@ * the set of cache names is pre-defined through {@link #setCacheNames}, with no * dynamic creation of further cache regions at runtime. * + *

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

Note: This is by no means a sophisticated CacheManager; it comes with no * cache configuration options. However, it may be useful for testing or simple * caching scenarios. For advanced local caching needs, consider - * {@link org.springframework.cache.jcache.JCacheCacheManager} or - * {@link org.springframework.cache.caffeine.CaffeineCacheManager}. + * {@link org.springframework.cache.caffeine.CaffeineCacheManager} or + * {@link org.springframework.cache.jcache.JCacheCacheManager}. * * @author Juergen Hoeller * @since 3.1 diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java index 926a44a29f9e..a54ed05f17a1 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ public CacheManager getCacheManager() { } @Override - public void afterPropertiesSet() { + public void afterPropertiesSet() { Assert.notNull(this.cacheManager, "CacheManager is required"); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index d20993ae27a7..2883826495f8 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,20 +32,16 @@ import org.springframework.util.ClassUtils; /** - * Abstract implementation of {@link CacheOperation} that caches attributes + * Abstract implementation of {@link CacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. target class; 3. declaring method; 4. declaring class/interface. * - *

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

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

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

As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ - private final Map> attributeCache = new ConcurrentHashMap<>(1024); + private final Map> operationCache = new ConcurrentHashMap<>(1024); /** - * Determine the caching attribute for this method invocation. - *

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

Defaults to class-declared metadata if no method-level metadata is found. * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) * @return {@link CacheOperation} for this method, or {@code null} if the method * is not cacheable */ @@ -90,21 +86,21 @@ public Collection getCacheOperations(Method method, @Nullable Cl } Object cacheKey = getCacheKey(method, targetClass); - Collection cached = this.attributeCache.get(cacheKey); + Collection cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? cached : null); + return (cached != NULL_CACHING_MARKER ? cached : null); } else { Collection cacheOps = computeCacheOperations(method, targetClass); if (cacheOps != null) { if (logger.isTraceEnabled()) { - logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps); + logger.trace("Adding cacheable method '" + method.getName() + "' with operations: " + cacheOps); } - this.attributeCache.put(cacheKey, cacheOps); + this.operationCache.put(cacheKey, cacheOps); } else { - this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return cacheOps; } @@ -129,7 +125,7 @@ private Collection computeCacheOperations(Method method, @Nullab return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); @@ -163,19 +159,19 @@ private Collection computeCacheOperations(Method method, @Nullab /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given class, if any. - * @param clazz the class to retrieve the attribute for - * @return all caching attribute associated with this class, or {@code null} if none + * @param clazz the class to retrieve the cache operations for + * @return all cache operations associated with this class, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Class clazz); /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given method, if any. - * @param method the method to retrieve the attribute for - * @return all caching attribute associated with this method, or {@code null} if none + * @param method the method to retrieve the cache operations for + * @return all cache operations associated with this method, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Method method); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index d99e31d87c2d..568aea4780b8 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,16 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.support.AopUtils; @@ -42,8 +47,14 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.KotlinDetector; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.SpringProperties; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -78,16 +89,48 @@ * @author Phillip Webb * @author Sam Brannen * @author Stephane Nicoll + * @author Sebastien Deleuze * @since 3.1 */ public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { + /** + * System property that instructs Spring's caching infrastructure to ignore the + * presence of Reactive Streams, in particular Reactor's {@link Mono}/{@link Flux} + * in {@link org.springframework.cache.annotation.Cacheable} method return type + * declarations. + *

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

Switch this flag to "true" in order to ignore Reactive Streams Publishers and + * process them as regular return values through synchronous caching, restoring 6.0 + * behavior. Note that this is not recommended and only works in very limited + * scenarios, e.g. with manual {@code Mono.cache()}/{@code Flux.cache()} calls. + * @since 6.1.3 + * @see org.reactivestreams.Publisher + */ + public static final String IGNORE_REACTIVESTREAMS_PROPERTY_NAME = "spring.cache.reactivestreams.ignore"; + + private static final boolean shouldIgnoreReactiveStreams = + SpringProperties.getFlag(IGNORE_REACTIVESTREAMS_PROPERTY_NAME); + + private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + "org.reactivestreams.Publisher", CacheAspectSupport.class.getClassLoader()); + + protected final Log logger = LogFactory.getLog(getClass()); private final Map metadataCache = new ConcurrentHashMap<>(1024); - private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); + private final StandardEvaluationContext originalEvaluationContext = new StandardEvaluationContext(); + + private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator( + new CacheEvaluationContextFactory(this.originalEvaluationContext)); + + @Nullable + private final ReactiveCachingHandler reactiveCachingHandler; @Nullable private CacheOperationSource cacheOperationSource; @@ -103,6 +146,12 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker private boolean initialized = false; + protected CacheAspectSupport() { + this.reactiveCachingHandler = + (reactiveStreamsPresent && !shouldIgnoreReactiveStreams ? new ReactiveCachingHandler() : null); + } + + /** * Configure this aspect with the given error handler, key generator and cache resolver/manager * suppliers, applying the corresponding default if a supplier is not resolvable. @@ -202,6 +251,7 @@ public void setCacheManager(CacheManager cacheManager) { @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); } @@ -337,7 +387,7 @@ protected void clearMetadataCache() { protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically) if (this.initialized) { - Class targetClass = getTargetClass(target); + Class targetClass = AopProxyUtils.ultimateTargetClass(target); CacheOperationSource cacheOperationSource = getCacheOperationSource(); if (cacheOperationSource != null) { Collection operations = cacheOperationSource.getCacheOperations(method, targetClass); @@ -348,7 +398,7 @@ protected Object execute(CacheOperationInvoker invoker, Object target, Method me } } - return invoker.invoke(); + return invokeOperation(invoker); } /** @@ -366,45 +416,126 @@ protected Object invokeOperation(CacheOperationInvoker invoker) { return invoker.invoke(); } - private Class getTargetClass(Object target) { - return AopProxyUtils.ultimateTargetClass(target); + @Nullable + private Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + if (contexts.isSynchronized()) { + // Special handling of synchronized invocation + return executeSynchronized(invoker, method, contexts); + } + + // Process any early evictions + processCacheEvicts(contexts.get(CacheEvictOperation.class), true, + CacheOperationExpressionEvaluator.NO_RESULT); + + // Check if we have a cached value matching the conditions + Object cacheHit = findCachedValue(invoker, method, contexts); + if (cacheHit == null || cacheHit instanceof Cache.ValueWrapper) { + return evaluate(cacheHit, invoker, method, contexts); + } + return cacheHit; } @Nullable - private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { - // Special handling of synchronized invocation - if (contexts.isSynchronized()) { - CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); + private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); + if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { + Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); + Cache cache = context.getCaches().iterator().next(); + if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) { + return cache.retrieve(key, () -> (CompletableFuture) invokeOperation(invoker)); + } + if (this.reactiveCachingHandler != null) { + Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key); + if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { + return returnValue; + } + } + try { + return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker)))); + } + catch (Cache.ValueRetrievalException ex) { + // Directly propagate ThrowableWrapper from the invoker, + // or potentially also an IllegalArgumentException etc. + ReflectionUtils.rethrowRuntimeException(ex.getCause()); + // Never reached + return null; + } + } + else { + // No caching required, just call the underlying method + return invokeOperation(invoker); + } + } + + /** + * Find a cached value only for {@link CacheableOperation} that passes the condition. + * @param contexts the cacheable operations + * @return a {@link Cache.ValueWrapper} holding the cached value, + * or {@code null} if none is found + */ + @Nullable + private Object findCachedValue(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + for (CacheOperationContext context : contexts.get(CacheableOperation.class)) { if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); - Cache cache = context.getCaches().iterator().next(); - try { - return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache)); + Object cached = findInCaches(context, key, invoker, method, contexts); + if (cached != null) { + if (logger.isTraceEnabled()) { + logger.trace("Cache entry for key '" + key + "' found in cache(s) " + context.getCacheNames()); + } + return cached; } - catch (Cache.ValueRetrievalException ex) { - // Directly propagate ThrowableWrapper from the invoker, - // or potentially also an IllegalArgumentException etc. - ReflectionUtils.rethrowRuntimeException(ex.getCause()); + else { + if (logger.isTraceEnabled()) { + logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames()); + } } } - else { - // No caching required, just call the underlying method - return invokeOperation(invoker); - } } + return null; + } - // Process any early evictions - processCacheEvicts(contexts.get(CacheEvictOperation.class), true, - CacheOperationExpressionEvaluator.NO_RESULT); + @Nullable + private Object findInCaches(CacheOperationContext context, Object key, + CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { - // Check if we have a cached value matching the conditions - Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); + for (Cache cache : context.getCaches()) { + if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) { + CompletableFuture result = cache.retrieve(key); + if (result != null) { + return result.exceptionally(ex -> { + getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); + return null; + }).thenCompose(value -> (CompletableFuture) evaluate( + (value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null), + invoker, method, contexts)); + } + else { + continue; + } + } + if (this.reactiveCachingHandler != null) { + Object returnValue = this.reactiveCachingHandler.findInCaches( + context, cache, key, invoker, method, contexts); + if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { + return returnValue; + } + } + Cache.ValueWrapper result = doGet(cache, key); + if (result != null) { + return result; + } + } + return null; + } - // Collect puts from any @Cacheable miss, if no cached value is found - List cachePutRequests = new ArrayList<>(1); - if (cacheHit == null) { - collectPutRequests(contexts.get(CacheableOperation.class), - CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); + @Nullable + private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method, + CacheOperationContexts contexts) { + + // Re-invocation in reactive pipeline after late cache hit determination? + if (contexts.processed) { + return cacheHit; } Object cacheValue; @@ -412,7 +543,7 @@ private Object execute(final CacheOperationInvoker invoker, Method method, Cache if (cacheHit != null && !hasCachePut(contexts)) { // If there are no put requests, just use the cache hit - cacheValue = cacheHit.get(); + cacheValue = unwrapCacheValue(cacheHit); returnValue = wrapCacheValue(method, cacheValue); } else { @@ -421,34 +552,39 @@ private Object execute(final CacheOperationInvoker invoker, Method method, Cache cacheValue = unwrapReturnValue(returnValue); } + // Collect puts from any @Cacheable miss, if no cached value is found + List cachePutRequests = new ArrayList<>(1); + if (cacheHit == null) { + collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, cachePutRequests); + } + // Collect any explicit @CachePuts collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); // Process any collected put requests, either from @CachePut or a @Cacheable miss for (CachePutRequest cachePutRequest : cachePutRequests) { - cachePutRequest.apply(cacheValue); + Object returnOverride = cachePutRequest.apply(cacheValue); + if (returnOverride != null) { + returnValue = returnOverride; + } } // Process any late evictions - processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); + Object returnOverride = processCacheEvicts( + contexts.get(CacheEvictOperation.class), false, returnValue); + if (returnOverride != null) { + returnValue = returnOverride; + } + + // Mark as processed for re-invocation after late cache hit determination + contexts.processed = true; return returnValue; } @Nullable - private Object handleSynchronizedGet(CacheOperationInvoker invoker, Object key, Cache cache) { - InvocationAwareResult invocationResult = new InvocationAwareResult(); - Object result = cache.get(key, () -> { - invocationResult.invoked = true; - if (logger.isTraceEnabled()) { - logger.trace("No cache entry for key '" + key + "' in cache " + cache.getName()); - } - return unwrapReturnValue(invokeOperation(invoker)); - }); - if (!invocationResult.invoked && logger.isTraceEnabled()) { - logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'"); - } - return result; + private Object unwrapCacheValue(@Nullable Object cacheValue) { + return (cacheValue instanceof Cache.ValueWrapper wrapper ? wrapper.get() : cacheValue); } @Nullable @@ -483,32 +619,55 @@ private boolean hasCachePut(CacheOperationContexts contexts) { return (cachePutContexts.size() != excluded.size()); } - private void processCacheEvicts( - Collection contexts, boolean beforeInvocation, @Nullable Object result) { + @Nullable + private Object processCacheEvicts(Collection contexts, boolean beforeInvocation, + @Nullable Object result) { - for (CacheOperationContext context : contexts) { - CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation; - if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) { - performCacheEvict(context, operation, result); + if (contexts.isEmpty()) { + return null; + } + List applicable = contexts.stream() + .filter(context -> (context.metadata.operation instanceof CacheEvictOperation evict && + beforeInvocation == evict.isBeforeInvocation())).toList(); + if (applicable.isEmpty()) { + return null; + } + + if (result instanceof CompletableFuture future) { + return future.whenComplete((value, ex) -> { + if (ex == null) { + performCacheEvicts(applicable, value); + } + }); + } + if (this.reactiveCachingHandler != null) { + Object returnValue = this.reactiveCachingHandler.processCacheEvicts(applicable, result); + if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { + return returnValue; } } + performCacheEvicts(applicable, result); + return null; } - private void performCacheEvict( - CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) { - - Object key = null; - for (Cache cache : context.getCaches()) { - if (operation.isCacheWide()) { - logInvalidating(context, operation, null); - doClear(cache, operation.isBeforeInvocation()); - } - else { - if (key == null) { - key = generateKey(context, result); + private void performCacheEvicts(List contexts, @Nullable Object result) { + for (CacheOperationContext context : contexts) { + CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation; + if (isConditionPassing(context, result)) { + Object key = context.getGeneratedKey(); + for (Cache cache : context.getCaches()) { + if (operation.isCacheWide()) { + logInvalidating(context, operation, null); + doClear(cache, operation.isBeforeInvocation()); + } + else { + if (key == null) { + key = generateKey(context, result); + } + logInvalidating(context, operation, key); + doEvict(cache, key, operation.isBeforeInvocation()); + } } - logInvalidating(context, operation, key); - doEvict(cache, key, operation.isBeforeInvocation()); } } } @@ -521,36 +680,10 @@ private void logInvalidating(CacheOperationContext context, CacheEvictOperation } /** - * Find a cached value only for {@link CacheableOperation} that passes the condition. - * @param contexts the cacheable operations - * @return a {@link Cache.ValueWrapper} holding the cached value, - * or {@code null} if none is found - */ - @Nullable - private Cache.ValueWrapper findCachedItem(Collection contexts) { - Object result = CacheOperationExpressionEvaluator.NO_RESULT; - for (CacheOperationContext context : contexts) { - if (isConditionPassing(context, result)) { - Object key = generateKey(context, result); - Cache.ValueWrapper cached = findInCaches(context, key); - if (cached != null) { - return cached; - } - else { - if (logger.isTraceEnabled()) { - logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames()); - } - } - } - } - return null; - } - - /** - * Collect the {@link CachePutRequest} for all {@link CacheOperation} using - * the specified result value. + * Collect a {@link CachePutRequest} for every {@link CacheOperation} + * using the specified result value. * @param contexts the contexts to handle - * @param result the result value (never {@code null}) + * @param result the result value * @param putRequests the collection to update */ private void collectPutRequests(Collection contexts, @@ -558,24 +691,9 @@ private void collectPutRequests(Collection contexts, for (CacheOperationContext context : contexts) { if (isConditionPassing(context, result)) { - Object key = generateKey(context, result); - putRequests.add(new CachePutRequest(context, key)); - } - } - } - - @Nullable - private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) { - for (Cache cache : context.getCaches()) { - Cache.ValueWrapper wrapper = doGet(cache, key); - if (wrapper != null) { - if (logger.isTraceEnabled()) { - logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'"); - } - return wrapper; + putRequests.add(new CachePutRequest(context)); } } - return null; } private boolean isConditionPassing(CacheOperationContext context, @Nullable Object result) { @@ -590,8 +708,10 @@ private boolean isConditionPassing(CacheOperationContext context, @Nullable Obje private Object generateKey(CacheOperationContext context, @Nullable Object result) { Object key = context.generateKey(result); if (key == null) { - throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " + - "using named params on classes without debug info?) " + context.metadata.operation); + throw new IllegalArgumentException(""" + Null key returned for cache operation [%s]. If you are using named parameters, \ + ensure that the compiler uses the '-parameters' flag.""" + .formatted(context.metadata.operation)); } if (logger.isTraceEnabled()) { logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation); @@ -606,6 +726,8 @@ private class CacheOperationContexts { private final boolean sync; + boolean processed; + public CacheOperationContexts(Collection operations, Method method, Object[] args, Object target, Class targetClass) { @@ -626,13 +748,13 @@ public boolean isSynchronized() { } private boolean determineSyncFlag(Method method) { - List cacheOperationContexts = this.contexts.get(CacheableOperation.class); - if (cacheOperationContexts == null) { // no @Cacheable operation at all + List cacheableContexts = this.contexts.get(CacheableOperation.class); + if (cacheableContexts == null) { // no @Cacheable operation at all return false; } boolean syncEnabled = false; - for (CacheOperationContext cacheOperationContext : cacheOperationContexts) { - if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) { + for (CacheOperationContext context : cacheableContexts) { + if (context.getOperation() instanceof CacheableOperation cacheable && cacheable.isSync()) { syncEnabled = true; break; } @@ -642,13 +764,13 @@ private boolean determineSyncFlag(Method method) { throw new IllegalStateException( "A sync=true operation cannot be combined with other cache operations on '" + method + "'"); } - if (cacheOperationContexts.size() > 1) { + if (cacheableContexts.size() > 1) { throw new IllegalStateException( "Only one sync=true operation is allowed on '" + method + "'"); } - CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next(); - CacheOperation operation = cacheOperationContext.getOperation(); - if (cacheOperationContext.getCaches().size() > 1) { + CacheOperationContext cacheableContext = cacheableContexts.iterator().next(); + CacheOperation operation = cacheableContext.getOperation(); + if (cacheableContext.getCaches().size() > 1) { throw new IllegalStateException( "A sync=true operation is restricted to a single cache on '" + operation + "'"); } @@ -716,6 +838,9 @@ protected class CacheOperationContext implements CacheOperationInvocationContext @Nullable private Boolean conditionPassing; + @Nullable + private Object key; + public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) { this.metadata = metadata; this.args = extractArgs(metadata.method, args); @@ -791,14 +916,27 @@ else if (this.metadata.operation instanceof CachePutOperation cachePutOperation) protected Object generateKey(@Nullable Object result) { if (StringUtils.hasText(this.metadata.operation.getKey())) { EvaluationContext evaluationContext = createEvaluationContext(result); - return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); + this.key = evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); + } + else { + this.key = this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); } - return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); + return this.key; + } + + /** + * Get generated key. + * @return generated key + * @since 6.1.2 + */ + @Nullable + protected Object getGeneratedKey() { + return this.key; } private EvaluationContext createEvaluationContext(@Nullable Object result) { return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args, - this.target, this.metadata.targetClass, this.metadata.targetMethod, result, beanFactory); + this.target, this.metadata.targetClass, this.metadata.targetMethod, result); } protected Collection getCaches() { @@ -819,27 +957,6 @@ private Collection prepareCacheNames(Collection caches) } - private class CachePutRequest { - - private final CacheOperationContext context; - - private final Object key; - - public CachePutRequest(CacheOperationContext context, Object key) { - this.context = context; - this.key = key; - } - - public void apply(@Nullable Object result) { - if (this.context.canPutToCache(result)) { - for (Cache cache : this.context.getCaches()) { - doPut(cache, this.key, result); - } - } - } - } - - private static final class CacheOperationCacheKey implements Comparable { private final CacheOperation cacheOperation; @@ -879,12 +996,199 @@ public int compareTo(CacheOperationCacheKey other) { } + private class CachePutRequest { + + private final CacheOperationContext context; + + public CachePutRequest(CacheOperationContext context) { + this.context = context; + } + + @Nullable + public Object apply(@Nullable Object result) { + if (result instanceof CompletableFuture future) { + return future.whenComplete((value, ex) -> { + if (ex == null) { + performCachePut(value); + } + }); + } + if (reactiveCachingHandler != null) { + Object returnValue = reactiveCachingHandler.processPutRequest(this, result); + if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { + return returnValue; + } + } + performCachePut(result); + return null; + } + + public void performCachePut(@Nullable Object value) { + if (this.context.canPutToCache(value)) { + Object key = this.context.getGeneratedKey(); + if (key == null) { + key = generateKey(this.context, value); + } + if (logger.isTraceEnabled()) { + logger.trace("Creating cache entry for key '" + key + "' in cache(s) " + + this.context.getCacheNames()); + } + for (Cache cache : this.context.getCaches()) { + doPut(cache, key, value); + } + } + } + } + + /** - * Internal holder class for recording that a cache method was invoked. + * Reactive Streams Subscriber for exhausting the Flux and collecting a List + * to cache. */ - private static class InvocationAwareResult { + private final class CachePutListSubscriber implements Subscriber { + + private final CachePutRequest request; + + private final List cacheValue = new ArrayList<>(); + + public CachePutListSubscriber(CachePutRequest request) { + this.request = request; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Integer.MAX_VALUE); + } + @Override + public void onNext(Object o) { + this.cacheValue.add(o); + } + @Override + public void onError(Throwable t) { + this.cacheValue.clear(); + } + @Override + public void onComplete() { + this.request.performCachePut(this.cacheValue); + } + } + + + /** + * Inner class to avoid a hard dependency on the Reactive Streams API at runtime. + */ + private class ReactiveCachingHandler { + + public static final Object NOT_HANDLED = new Object(); + + private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); - boolean invoked; + @Nullable + public Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) { + ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType()); + if (adapter != null) { + if (adapter.isMultiValue()) { + // Flux or similar + return adapter.fromPublisher(Flux.from(Mono.fromFuture( + cache.retrieve(key, + () -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().toFuture()))) + .flatMap(Flux::fromIterable)); + } + else { + // Mono or similar + return adapter.fromPublisher(Mono.fromFuture( + cache.retrieve(key, + () -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture()))); + } + } + if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) { + return Mono.fromFuture(cache.retrieve(key, () -> { + Mono mono = ((Mono) invokeOperation(invoker)); + if (mono == null) { + mono = Mono.empty(); + } + return mono.toFuture(); + })); + } + return NOT_HANDLED; + } + + @Nullable + public Object processCacheEvicts(List contexts, @Nullable Object result) { + ReactiveAdapter adapter = (result != null ? this.registry.getAdapter(result.getClass()) : null); + if (adapter != null) { + return adapter.fromPublisher(Mono.from(adapter.toPublisher(result)) + .doOnSuccess(value -> performCacheEvicts(contexts, value))); + } + return NOT_HANDLED; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Nullable + public Object findInCaches(CacheOperationContext context, Cache cache, Object key, + CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + + ReactiveAdapter adapter = this.registry.getAdapter(context.getMethod().getReturnType()); + if (adapter != null) { + CompletableFuture cachedFuture = cache.retrieve(key); + if (cachedFuture == null) { + return null; + } + if (adapter.isMultiValue()) { + return adapter.fromPublisher(Flux.from(Mono.fromFuture(cachedFuture)) + .switchIfEmpty(Flux.defer(() -> (Flux) evaluate(null, invoker, method, contexts))) + .flatMap(v -> evaluate(valueToFlux(v, contexts), invoker, method, contexts)) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); + return evaluate(null, invoker, method, contexts); + } + catch (RuntimeException exception) { + return Flux.error(exception); + } + })); + } + else { + return adapter.fromPublisher(Mono.fromFuture(cachedFuture) + .switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts))) + .flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts)) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); + return evaluate(null, invoker, method, contexts); + } + catch (RuntimeException exception) { + return Mono.error(exception); + } + })); + } + } + return NOT_HANDLED; + } + + private Flux valueToFlux(Object value, CacheOperationContexts contexts) { + Object data = unwrapCacheValue(value); + return (!contexts.processed && data instanceof Iterable iterable ? Flux.fromIterable(iterable) : + (data != null ? Flux.just(data) : Flux.empty())); + } + + @Nullable + public Object processPutRequest(CachePutRequest request, @Nullable Object result) { + ReactiveAdapter adapter = (result != null ? this.registry.getAdapter(result.getClass()) : null); + if (adapter != null) { + if (adapter.isMultiValue()) { + Flux source = Flux.from(adapter.toPublisher(result)) + .publish().refCount(2); + source.subscribe(new CachePutListSubscriber(request)); + return adapter.fromPublisher(source); + } + else { + return adapter.fromPublisher(Mono.from(adapter.toPublisher(result)) + .doOnSuccess(request::performCachePut)); + } + } + return NOT_HANDLED; + } } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java index 25d4282313e1..29604d91b648 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,12 +25,12 @@ import org.springframework.lang.Nullable; /** - * Cache specific evaluation context that adds a method parameters as SpEL - * variables, in a lazy manner. The lazy nature eliminates unneeded - * parsing of classes byte code for parameter discovery. + * Cache-specific evaluation context that adds method parameters as SpEL + * variables, in a lazy manner. The lazy nature avoids unnecessary + * parsing of a class's byte code for parameter discovery. * - *

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

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

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

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

This permits the validation of expressions that could potentially access + * a variable even when such a variable isn't available yet. Any expression * trying to use that variable should therefore fail to evaluate. */ public void addUnavailableVariable(String name) { diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java new file mode 100644 index 000000000000..327cfd4d9768 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.function.SingletonSupplier; + +/** + * A factory for {@link CacheEvaluationContext} that makes sure that internal + * delegates are reused. + * + * @author Stephane Nicoll + * @since 6.1.1 + */ +class CacheEvaluationContextFactory { + + private final StandardEvaluationContext originalContext; + + @Nullable + private Supplier parameterNameDiscoverer; + + CacheEvaluationContextFactory(StandardEvaluationContext originalContext) { + this.originalContext = originalContext; + } + + public void setParameterNameDiscoverer(Supplier parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + public ParameterNameDiscoverer getParameterNameDiscoverer() { + if (this.parameterNameDiscoverer == null) { + this.parameterNameDiscoverer = SingletonSupplier.of(new DefaultParameterNameDiscoverer()); + } + return this.parameterNameDiscoverer.get(); + } + + /** + * Creates a {@link CacheEvaluationContext} for the specified operation. + * @param rootObject the {@code root} object to use for the context + * @param targetMethod the target cache {@link Method} + * @param args the arguments of the method invocation + * @return a context suitable for this cache operation + */ + public CacheEvaluationContext forOperation(CacheExpressionRootObject rootObject, + Method targetMethod, Object[] args) { + + CacheEvaluationContext evaluationContext = new CacheEvaluationContext( + rootObject, targetMethod, args, getParameterNameDiscoverer()); + this.originalContext.applyDelegatesTo(evaluationContext); + return evaluationContext; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java index 2488fa4c54b3..2f1f56f14a7d 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ * * @author Costin Leau * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 3.1 */ @SuppressWarnings("serial") diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java index 82892d0ccfb2..13a49ea1026b 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.factory.BeanFactory; import org.springframework.cache.Cache; import org.springframework.context.expression.AnnotatedElementKey; -import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.CachedExpressionEvaluator; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -67,6 +65,13 @@ class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { private final Map unlessCache = new ConcurrentHashMap<>(64); + private final CacheEvaluationContextFactory evaluationContextFactory; + + public CacheOperationExpressionEvaluator(CacheEvaluationContextFactory evaluationContextFactory) { + super(); + this.evaluationContextFactory = evaluationContextFactory; + this.evaluationContextFactory.setParameterNameDiscoverer(this::getParameterNameDiscoverer); + } /** * Create an {@link EvaluationContext}. @@ -81,21 +86,18 @@ class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { */ public EvaluationContext createEvaluationContext(Collection caches, Method method, Object[] args, Object target, Class targetClass, Method targetMethod, - @Nullable Object result, @Nullable BeanFactory beanFactory) { + @Nullable Object result) { CacheExpressionRootObject rootObject = new CacheExpressionRootObject( caches, method, args, target, targetClass); - CacheEvaluationContext evaluationContext = new CacheEvaluationContext( - rootObject, targetMethod, args, getParameterNameDiscoverer()); + CacheEvaluationContext evaluationContext = this.evaluationContextFactory + .forOperation(rootObject, targetMethod, args); if (result == RESULT_UNAVAILABLE) { evaluationContext.addUnavailableVariable(RESULT_VARIABLE); } else if (result != NO_RESULT) { evaluationContext.setVariable(RESULT_VARIABLE, result); } - if (beanFactory != null) { - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); - } return evaluationContext; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java index cfaab08137bd..e37736480e51 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ * Abstract the invocation of a cache operation. * *

Does not provide a way to transmit checked exceptions but - * provide a special exception that should be used to wrap any + * provides a special exception that should be used to wrap any * exception that was thrown by the underlying invocation. * Callers are expected to handle this issue type specifically. * diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java index 02a9b4f41646..7316831e2497 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ default boolean isCandidateClass(Class targetClass) { * Return the collection of cache operations for this method, * or {@code null} if the method contains no cacheable annotations. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return all cache operations for this method, or {@code null} if none found */ diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java index e70275aeaed7..6fe6d7c46cfb 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ /** * A {@code Pointcut} that matches if the underlying {@link CacheOperationSource} - * has an attribute for a given method. + * has an operation for a given method. * * @author Costin Leau * @author Juergen Hoeller @@ -36,7 +36,7 @@ * @since 3.1 */ @SuppressWarnings("serial") -class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { +final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { @Nullable private CacheOperationSource cacheOperationSource; @@ -78,7 +78,7 @@ public String toString() { * {@link ClassFilter} that delegates to {@link CacheOperationSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class CacheOperationSourceClassFilter implements ClassFilter { + private final class CacheOperationSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -88,6 +88,7 @@ public boolean matches(Class clazz) { return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); } + @Nullable private CacheOperationSource getCacheOperationSource() { return cacheOperationSource; } @@ -95,7 +96,7 @@ private CacheOperationSource getCacheOperationSource() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof CacheOperationSourceClassFilter that && - ObjectUtils.nullSafeEquals(cacheOperationSource, that.getCacheOperationSource()))); + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); } @Override @@ -105,9 +106,8 @@ public int hashCode() { @Override public String toString() { - return CacheOperationSourceClassFilter.class.getName() + ": " + cacheOperationSource; + return CacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); } - } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java index a8023cea59cb..61cfc4b469db 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java @@ -52,6 +52,7 @@ public void setCacheNames(Collection cacheNames) { } @Override + @Nullable protected Collection getCacheNames(CacheOperationInvocationContext context) { return this.cacheNames; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java index 6dd60e6934b2..d0f2a64ce82c 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java @@ -23,7 +23,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * A simple key as returned from the {@link SimpleKeyGenerator}. @@ -73,7 +72,7 @@ public final int hashCode() { @Override public String toString() { - return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]"; + return getClass().getSimpleName() + " " + Arrays.deepToString(this.params); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java index f443ea91a578..c2365ad9e651 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,9 @@ package org.springframework.cache.interceptor; import java.lang.reflect.Method; +import java.util.Arrays; + +import org.springframework.core.KotlinDetector; /** * Simple key generator. Returns the parameter itself if a single non-null @@ -30,6 +33,7 @@ * * @author Phillip Webb * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 4.0 * @see SimpleKey * @see org.springframework.cache.annotation.CachingConfigurer @@ -38,7 +42,8 @@ public class SimpleKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { - return generateKey(params); + return generateKey((KotlinDetector.isSuspendingFunction(method) ? + Arrays.copyOf(params, params.length - 1) : params)); } /** diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java index b4ce3c2b0e2c..48a30ae1f8bd 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ import org.springframework.expression.EvaluationException; /** - * A specific {@link EvaluationException} to mention that a given variable - * used in the expression is not available in the context. + * An internal {@link EvaluationException} which signals that a given variable + * used in an expression is not available in the context. * * @author Stephane Nicoll * @since 4.0.6 @@ -28,17 +28,8 @@ @SuppressWarnings("serial") class VariableNotAvailableException extends EvaluationException { - private final String name; - - public VariableNotAvailableException(String name) { - super("Variable not available"); - this.name = name; - } - - - public final String getName() { - return this.name; + super("Variable '" + name + "' not available"); } } diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java index 6c814ff18e0f..b8746e97f91a 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package org.springframework.cache.support; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; import org.springframework.cache.Cache; import org.springframework.lang.Nullable; @@ -80,6 +82,17 @@ public T get(Object key, Callable valueLoader) { } } + @Override + @Nullable + public CompletableFuture retrieve(Object key) { + return null; + } + + @Override + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + return valueLoader.get(); + } + @Override public void put(Object key, @Nullable Object value) { } diff --git a/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java b/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java index 700936a85a89..460d27a049c3 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java +++ b/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.cache.support; +import java.util.Objects; + import org.springframework.cache.Cache.ValueWrapper; import org.springframework.lang.Nullable; @@ -50,4 +52,19 @@ public Object get() { return this.value; } + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ValueWrapper wrapper && Objects.equals(get(), wrapper.get()))); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.value); + } + + @Override + public String toString() { + return "ValueWrapper for [" + this.value + "]"; + } + } diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java index 779b73c1c4ea..30cdd4e4d46a 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ * @see org.springframework.context.ApplicationEvent * @see org.springframework.context.event.ApplicationEventMulticaster * @see org.springframework.context.event.EventPublicationInterceptor + * @see org.springframework.transaction.event.TransactionalApplicationListener */ @FunctionalInterface public interface ApplicationEventPublisher { @@ -42,8 +43,21 @@ public interface ApplicationEventPublisher { * or even immediate execution at all. Event listeners are encouraged * to be as efficient as possible, individually using asynchronous * execution for longer-running and potentially blocking operations. + *

For usage in a reactive call stack, include event publication + * as a simple hand-off: + * {@code Mono.fromRunnable(() -> eventPublisher.publishEvent(...))}. + * As with any asynchronous execution, thread-local data is not going + * to be available for reactive listener methods. All state which is + * necessary to process the event needs to be included in the event + * instance itself. + *

For the convenient inclusion of the current transaction context + * in a reactive hand-off, consider using + * {@link org.springframework.transaction.reactive.TransactionalEventPublisher#publishEvent(Function)}. + * For thread-bound transactions, this is not necessary since the + * state will be implicitly available through thread-local storage. * @param event the event to publish * @see #publishEvent(Object) + * @see ApplicationListener#supportsAsyncExecution() * @see org.springframework.context.event.ContextRefreshedEvent * @see org.springframework.context.event.ContextClosedEvent */ @@ -61,6 +75,11 @@ default void publishEvent(ApplicationEvent event) { * or even immediate execution at all. Event listeners are encouraged * to be as efficient as possible, individually using asynchronous * execution for longer-running and potentially blocking operations. + *

For the convenient inclusion of the current transaction context + * in a reactive hand-off, consider using + * {@link org.springframework.transaction.reactive.TransactionalEventPublisher#publishEvent(Object)}. + * For thread-bound transactions, this is not necessary since the + * state will be implicitly available through thread-local storage. * @param event the event to publish * @since 4.2 * @see #publishEvent(ApplicationEvent) diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationListener.java b/spring-context/src/main/java/org/springframework/context/ApplicationListener.java index 7be5b678d361..e73479ff0186 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationListener.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationListener.java @@ -48,6 +48,18 @@ public interface ApplicationListener extends EventLi */ void onApplicationEvent(E event); + /** + * Return whether this listener supports asynchronous execution. + * @return {@code true} if this listener instance can be executed asynchronously + * depending on the multicaster configuration (the default), or {@code false} if it + * needs to immediately run within the original thread which published the event + * @since 6.1 + * @see org.springframework.context.event.SimpleApplicationEventMulticaster#setTaskExecutor + */ + default boolean supportsAsyncExecution() { + return true; + } + /** * Create a new {@code ApplicationListener} for the given payload consumer. diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java index 06f98a4048ca..5ad500d1b211 100644 --- a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,7 +121,7 @@ default void stop(Runnable callback) { /** * Return the phase that this lifecycle object is supposed to run in. *

The default implementation returns {@link #DEFAULT_PHASE} in order to - * let {@code stop()} callbacks execute after regular {@code Lifecycle} + * let {@code stop()} callbacks execute before regular {@code Lifecycle} * implementations. * @see #isAutoStartup() * @see #start() diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java index 286059addd2e..3ac5edfa5952 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,7 +100,6 @@ public final BeanDefinitionRegistry getRegistry() { * Set the {@code Environment} to use when evaluating whether * {@link Conditional @Conditional}-annotated component classes should be registered. *

The default is a {@link StandardEnvironment}. - * @see #registerBean(Class, String, Class...) */ public void setEnvironment(Environment environment) { this.conditionEvaluator = new ConditionEvaluator(this.registry, environment, null); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java index 87223f353734..4f0f8a7e62b5 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java @@ -16,16 +16,27 @@ package org.springframework.context.annotation; +import java.lang.annotation.Annotation; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -41,8 +52,10 @@ * themselves annotated with {@code @Component}. * *

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

If the annotation's value doesn't indicate a bean name, an appropriate * name will be built based on the short name of the class (with the first @@ -53,6 +66,7 @@ * * @author Juergen Hoeller * @author Mark Fisher + * @author Sam Brannen * @since 2.5 * @see org.springframework.stereotype.Component#value() * @see org.springframework.stereotype.Repository#value() @@ -72,9 +86,24 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component"; + private static final Adapt[] ADAPTATIONS = Adapt.values(false, true); + + + private static final Log logger = LogFactory.getLog(AnnotationBeanNameGenerator.class); + + /** + * Set used to track which stereotype annotations have already been checked + * to see if they use a convention-based override for the {@code value} + * attribute in {@code @Component}. + * @since 6.1 + * @see #determineBeanNameFromAnnotation(AnnotatedBeanDefinition) + */ + private static final Set conventionBasedStereotypeCheckCache = ConcurrentHashMap.newKeySet(); + private final Map> metaAnnotationTypesCache = new ConcurrentHashMap<>(); + @Override public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { if (definition instanceof AnnotatedBeanDefinition annotatedBeanDefinition) { @@ -95,24 +124,45 @@ public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry */ @Nullable protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) { - AnnotationMetadata amd = annotatedDef.getMetadata(); - Set types = amd.getAnnotationTypes(); - String beanName = null; - for (String type : types) { - AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type); - if (attributes != null) { - Set metaTypes = this.metaAnnotationTypesCache.computeIfAbsent(type, key -> { - Set result = amd.getMetaAnnotationTypes(key); - return (result.isEmpty() ? Collections.emptySet() : result); - }); - if (isStereotypeWithNameValue(type, metaTypes, attributes)) { + AnnotationMetadata metadata = annotatedDef.getMetadata(); + + String beanName = getExplicitBeanName(metadata); + if (beanName != null) { + return beanName; + } + + // List of annotations directly present on the class we're searching on. + // MergedAnnotation implementations do not implement equals()/hashCode(), + // so we use a List and a 'visited' Set below. + List> mergedAnnotations = metadata.getAnnotations().stream() + .filter(MergedAnnotation::isDirectlyPresent) + .toList(); + + Set visited = new HashSet<>(); + + for (MergedAnnotation mergedAnnotation : mergedAnnotations) { + AnnotationAttributes attributes = mergedAnnotation.asAnnotationAttributes(ADAPTATIONS); + if (visited.add(attributes)) { + String annotationType = mergedAnnotation.getType().getName(); + Set metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType, + key -> getMetaAnnotationTypes(mergedAnnotation)); + if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) { Object value = attributes.get("value"); - if (value instanceof String strVal && !strVal.isEmpty()) { - if (beanName != null && !strVal.equals(beanName)) { + if (value instanceof String currentName && !currentName.isBlank()) { + if (conventionBasedStereotypeCheckCache.add(annotationType) && + metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) { + logger.warn(""" + Support for convention-based stereotype names is deprecated and will \ + be removed in a future version of the framework. Please annotate the \ + 'value' attribute in @%s with @AliasFor(annotation=Component.class) \ + to declare an explicit alias for @Component's 'value' attribute.""" + .formatted(annotationType)); + } + if (beanName != null && !currentName.equals(beanName)) { throw new IllegalStateException("Stereotype annotations suggest inconsistent " + - "component names: '" + beanName + "' versus '" + strVal + "'"); + "component names: '" + beanName + "' versus '" + currentName + "'"); } - beanName = strVal; + beanName = currentName; } } } @@ -120,23 +170,61 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat return beanName; } + private Set getMetaAnnotationTypes(MergedAnnotation mergedAnnotation) { + Set result = MergedAnnotations.from(mergedAnnotation.getType()).stream() + .map(metaAnnotation -> metaAnnotation.getType().getName()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return (result.isEmpty() ? Collections.emptySet() : result); + } + + /** + * Get the explicit bean name for the underlying class, as configured via + * {@link org.springframework.stereotype.Component @Component} and taking into + * account {@link org.springframework.core.annotation.AliasFor @AliasFor} + * semantics for annotation attribute overrides for {@code @Component}'s + * {@code value} attribute. + * @param metadata the {@link AnnotationMetadata} for the underlying class + * @return the explicit bean name, or {@code null} if not found + * @since 6.1 + * @see org.springframework.stereotype.Component#value() + */ + @Nullable + private String getExplicitBeanName(AnnotationMetadata metadata) { + List names = metadata.getAnnotations().stream(COMPONENT_ANNOTATION_CLASSNAME) + .map(annotation -> annotation.getString(MergedAnnotation.VALUE)) + .filter(StringUtils::hasText) + .map(String::trim) + .distinct() + .toList(); + + if (names.size() == 1) { + return names.get(0); + } + if (names.size() > 1) { + throw new IllegalStateException( + "Stereotype annotations suggest inconsistent component names: " + names); + } + return null; + } + /** * Check whether the given annotation is a stereotype that is allowed - * to suggest a component name through its annotation {@code value()}. + * to suggest a component name through its {@code value()} attribute. * @param annotationType the name of the annotation class to check * @param metaAnnotationTypes the names of meta-annotations on the given annotation * @param attributes the map of attributes for the given annotation * @return whether the annotation qualifies as a stereotype with component name */ protected boolean isStereotypeWithNameValue(String annotationType, - Set metaAnnotationTypes, @Nullable Map attributes) { + Set metaAnnotationTypes, Map attributes) { - boolean isStereotype = annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) || - metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) || + boolean isStereotype = metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) || annotationType.equals("jakarta.annotation.ManagedBean") || - annotationType.equals("jakarta.inject.Named"); + annotationType.equals("javax.annotation.ManagedBean") || + annotationType.equals("jakarta.inject.Named") || + annotationType.equals("javax.inject.Named"); - return (isStereotype && attributes != null && attributes.containsKey("value")); + return (isStereotype && attributes.containsKey("value")); } /** diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index 30b31b7fc9a5..903984ab0fdd 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -16,10 +16,10 @@ package org.springframework.context.annotation; -import java.util.Collections; +import java.lang.annotation.Annotation; import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; @@ -33,6 +33,7 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.Nullable; @@ -50,6 +51,7 @@ * @author Chris Beams * @author Phillip Webb * @author Stephane Nicoll + * @author Sam Brannen * @since 2.5 * @see ContextAnnotationAutowireCandidateResolver * @see ConfigurationClassPostProcessor @@ -271,48 +273,27 @@ static BeanDefinitionHolder applyScopedProxyMode( } @Nullable - static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, Class annotationClass) { - return attributesFor(metadata, annotationClass.getName()); + static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, Class annotationType) { + return attributesFor(metadata, annotationType.getName()); } @Nullable - static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String annotationClassName) { - return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annotationClassName)); + static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String annotationTypeName) { + return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annotationTypeName)); } static Set attributesForRepeatable(AnnotationMetadata metadata, - Class containerClass, Class annotationClass) { + Class annotationType, Class containerType, + Predicate> predicate) { - return attributesForRepeatable(metadata, containerClass.getName(), annotationClass.getName()); + return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, predicate, false, false); } - @SuppressWarnings("unchecked") - static Set attributesForRepeatable( - AnnotationMetadata metadata, String containerClassName, String annotationClassName) { - - Set result = new LinkedHashSet<>(); - - // Direct annotation present? - addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName)); - - // Container annotation present? - Map container = metadata.getAnnotationAttributes(containerClassName); - if (container != null && container.containsKey("value")) { - for (Map containedAttributes : (Map[]) container.get("value")) { - addAttributesIfNotNull(result, containedAttributes); - } - } - - // Return merged result - return Collections.unmodifiableSet(result); - } - - private static void addAttributesIfNotNull( - Set result, @Nullable Map attributes) { + static Set attributesForRepeatable(AnnotationMetadata metadata, + Class annotationType, Class containerType, + boolean sortByReversedMetaDistance) { - if (attributes != null) { - result.add(AnnotationAttributes.fromMap(attributes)); - } + return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false, sortByReversedMetaDistance); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java index 14732b178339..bbc64c4b359a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -135,9 +135,9 @@ *

{@code @Bean} Lite Mode

* *

{@code @Bean} methods may also be declared within classes that are not - * annotated with {@code @Configuration}. For example, bean methods may be declared - * in a {@code @Component} class or even in a plain old class. In such cases, - * a {@code @Bean} method will get processed in a so-called 'lite' mode. + * annotated with {@code @Configuration}. If a bean method is declared on a bean + * that is not annotated with {@code @Configuration} it is processed in a + * so-called 'lite' mode. * *

Bean methods in lite mode will be treated as plain factory * methods by the container (similar to {@code factory-method} declarations diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java index 5f01df18b1a2..ea09841aee31 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -42,8 +42,13 @@ final class BeanMethod extends ConfigurationMethod { @Override public void validate(ProblemReporter problemReporter) { + if ("void".equals(getMetadata().getReturnTypeName())) { + // declared as void: potential misuse of @Bean, maybe meant as init method instead? + problemReporter.error(new VoidDeclaredMethodError()); + } + if (getMetadata().isStatic()) { - // static @Bean methods have no constraints to validate -> return immediately + // static @Bean methods have no further constraints to validate -> return immediately return; } @@ -71,6 +76,15 @@ public String toString() { } + private class VoidDeclaredMethodError extends Problem { + + VoidDeclaredMethodError() { + super("@Bean method '%s' must not be declared as void; change the method's return type or its annotation." + .formatted(getMetadata().getMethodName()), getResourceLocation()); + } + } + + private class NonOverridableMethodError extends Problem { NonOverridableMethodError() { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index be2de0956c97..7aa22b2eb7ad 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -33,6 +33,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; /** @@ -336,14 +337,25 @@ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) if (!this.registry.containsBeanDefinition(beanName)) { return true; } + BeanDefinition existingDef = this.registry.getBeanDefinition(beanName); BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition(); if (originatingDef != null) { existingDef = originatingDef; } + + // Explicitly registered overriding bean? + if (!(existingDef instanceof ScannedGenericBeanDefinition) && + (this.registry.isBeanDefinitionOverridable(beanName) || ObjectUtils.nullSafeEquals( + beanDefinition.getBeanClassName(), existingDef.getBeanClassName()))) { + return false; + } + + // Scanned same file or equivalent class twice? if (isCompatible(beanDefinition, existingDef)) { return false; } + throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName + "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " + "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]"); @@ -361,9 +373,8 @@ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) * new definition to be skipped in favor of the existing definition */ protected boolean isCompatible(BeanDefinition newDef, BeanDefinition existingDef) { - return (!(existingDef instanceof ScannedGenericBeanDefinition) || // explicitly registered overriding bean - (newDef.getSource() != null && newDef.getSource().equals(existingDef.getSource())) || // scanned same file twice - newDef.equals(existingDef)); // scanned equivalent class twice + return ((newDef.getSource() != null && newDef.getSource().equals(existingDef.getSource())) || + newDef.equals(existingDef)); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index 7c717714c75a..daeb1cd833e1 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.context.index.CandidateComponentsIndex; import org.springframework.context.index.CandidateComponentsIndexLoader; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; @@ -47,6 +48,7 @@ import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.ClassFormatException; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AnnotationTypeFilter; @@ -62,11 +64,13 @@ import org.springframework.util.ClassUtils; /** - * A component provider that provides candidate components from a base package. Can - * use {@link CandidateComponentsIndex the index} if it is available of scans the - * classpath otherwise. Candidate components are identified by applying exclude and - * include filters. {@link AnnotationTypeFilter}, {@link AssignableTypeFilter} include - * filters on an annotation/superclass that are annotated with {@link Indexed} are + * A component provider that scans for candidate components starting from a + * specified base package. Can use the {@linkplain CandidateComponentsIndex component + * index}, if it is available, and scans the classpath otherwise. + * + *

Candidate components are identified by applying exclude and include filters. + * {@link AnnotationTypeFilter} and {@link AssignableTypeFilter} include filters + * for an annotation/target-type that is annotated with {@link Indexed} are * supported: if any other include filter is specified, the index is ignored and * classpath scanning is used instead. * @@ -86,10 +90,23 @@ * @see ScannedGenericBeanDefinition * @see CandidateComponentsIndex */ +@SuppressWarnings("removal") // components index public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + /** + * System property that instructs Spring to ignore class format exceptions during + * classpath scanning, in particular for unsupported class file versions. + * By default, such a class format mismatch leads to a classpath scanning failure. + * @since 6.1.2 + * @see ClassFormatException + */ + public static final String IGNORE_CLASSFORMAT_PROPERTY_NAME = "spring.classformat.ignore"; + + private static final boolean shouldIgnoreClassFormatException = + SpringProperties.getFlag(IGNORE_CLASSFORMAT_PROPERTY_NAME); + protected final Log logger = LogFactory.getLog(getClass()); @@ -200,8 +217,9 @@ public void resetFilters(boolean useDefaultFilters) { * {@link Repository @Repository}, {@link Service @Service}, and * {@link Controller @Controller} stereotype annotations. *

Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and - * JSR-330's {@link jakarta.inject.Named} annotations, if available. - * + * JSR-330's {@link jakarta.inject.Named} annotations (as well as their + * pre-Jakarta {@code javax.annotation.ManagedBean} and {@code javax.inject.Named} + * equivalents), if available. */ @SuppressWarnings("unchecked") protected void registerDefaultFilters() { @@ -215,11 +233,27 @@ protected void registerDefaultFilters() { catch (ClassNotFoundException ex) { // JSR-250 1.1 API (as included in Jakarta EE) not available - simply skip. } + try { + this.includeFilters.add(new AnnotationTypeFilter( + ((Class) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)); + logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning"); + } + catch (ClassNotFoundException ex) { + // JSR-250 1.1 API not available - simply skip. + } try { this.includeFilters.add(new AnnotationTypeFilter( ((Class) ClassUtils.forName("jakarta.inject.Named", cl)), false)); logger.trace("JSR-330 'jakarta.inject.Named' annotation found and supported for component scanning"); } + catch (ClassNotFoundException ex) { + // JSR-330 API (as included in Jakarta EE) not available - simply skip. + } + try { + this.includeFilters.add(new AnnotationTypeFilter( + ((Class) ClassUtils.forName("javax.inject.Named", cl)), false)); + logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning"); + } catch (ClassNotFoundException ex) { // JSR-330 API not available - simply skip. } @@ -305,7 +339,7 @@ public final MetadataReaderFactory getMetadataReaderFactory() { /** - * Scan the class path for candidate components. + * Scan the component index or class path for candidate components. * @param basePackage the package to check for annotated classes * @return a corresponding Set of autodetected bean definitions */ @@ -319,7 +353,7 @@ public Set findCandidateComponents(String basePackage) { } /** - * Determine if the index can be used by this instance. + * Determine if the component index can be used by this instance. * @return {@code true} if the index is available and the configuration of this * instance is supported by it, {@code false} otherwise * @since 5.0 @@ -344,7 +378,8 @@ private boolean indexSupportsIncludeFilter(TypeFilter filter) { if (filter instanceof AnnotationTypeFilter annotationTypeFilter) { Class annotationType = annotationTypeFilter.getAnnotationType(); return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) || - annotationType.getName().startsWith("jakarta.")); + annotationType.getName().startsWith("jakarta.") || + annotationType.getName().startsWith("javax.")); } if (filter instanceof AssignableTypeFilter assignableTypeFilter) { Class target = assignableTypeFilter.getTargetType(); @@ -459,9 +494,20 @@ private Set scanCandidateComponents(String basePackage) { logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage()); } } + catch (ClassFormatException ex) { + if (shouldIgnoreClassFormatException) { + if (debugEnabled) { + logger.debug("Ignored incompatible class format in " + resource + ": " + ex.getMessage()); + } + } + else { + throw new BeanDefinitionStoreException("Incompatible class format in " + resource + + ": set system property 'spring.classformat.ignore' to 'true' " + + "if you mean to ignore such files during classpath scanning", ex); + } + } catch (Throwable ex) { - throw new BeanDefinitionStoreException( - "Failed to read candidate component class: " + resource, ex); + throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex); } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index 32d0cb9451dc..15f044bbc398 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -35,6 +36,13 @@ import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aot.generate.AccessControl; +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.support.ClassHintUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.BeanCreationException; @@ -43,20 +51,27 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor; import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.BridgeMethodResolver; -import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; import org.springframework.jndi.support.SimpleJndiBeanFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -298,12 +313,45 @@ public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, C metadata.checkConfigMembers(beanDefinition); } + @Override + @Nullable + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + BeanRegistrationAotContribution parentAotContribution = super.processAheadOfTime(registeredBean); + Class beanClass = registeredBean.getBeanClass(); + String beanName = registeredBean.getBeanName(); + RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); + InjectionMetadata metadata = findResourceMetadata(beanName, beanClass, + beanDefinition.getPropertyValues()); + Collection injectedElements = getInjectedElements(metadata, + beanDefinition.getPropertyValues()); + if (!ObjectUtils.isEmpty(injectedElements)) { + AotContribution aotContribution = new AotContribution(beanClass, injectedElements, + getAutowireCandidateResolver(registeredBean)); + return BeanRegistrationAotContribution.concat(parentAotContribution, aotContribution); + } + return parentAotContribution; + } + + @Nullable + private AutowireCandidateResolver getAutowireCandidateResolver(RegisteredBean registeredBean) { + if (registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory lbf) { + return lbf.getAutowireCandidateResolver(); + } + return null; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Collection getInjectedElements(InjectionMetadata metadata, PropertyValues propertyValues) { + return (Collection) metadata.getInjectedElements(propertyValues); + } + @Override public void resetBeanDefinition(String beanName) { this.injectionMetadataCache.remove(beanName); } @Override + @Nullable public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { return null; } @@ -325,6 +373,29 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str return pvs; } + /** + * Native processing method for direct calls with an arbitrary target + * instance, resolving all of its fields and methods which are annotated with + * one of the supported 'resource' annotation types. + * @param bean the target instance to process + * @throws BeanCreationException if resource injection failed + * @since 6.1.3 + */ + public void processInjection(Object bean) throws BeanCreationException { + Class clazz = bean.getClass(); + InjectionMetadata metadata = findResourceMetadata(clazz.getName(), clazz, null); + try { + metadata.inject(bean, null, null); + } + catch (BeanCreationException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException( + "Injection of resource dependencies failed for class [" + clazz + "]", ex); + } + } + private InjectionMetadata findResourceMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers. @@ -387,8 +458,8 @@ else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceTyp if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } - if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { - if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) { + if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static methods"); } @@ -398,7 +469,9 @@ else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceTyp PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new EjbRefElement(method, bridgedMethod, pd)); } - else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) { + } + else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } @@ -411,7 +484,9 @@ else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakart currElements.add(new ResourceElement(method, bridgedMethod, pd)); } } - else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) { + } + else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } @@ -452,16 +527,9 @@ public Class getTargetClass() { return element.lookupType; } @Override - public boolean isStatic() { - return false; - } - @Override public Object getTarget() { return getResource(element, requestingBeanName); } - @Override - public void releaseTarget(Object target) { - } }; ProxyFactory pf = new ProxyFactory(); @@ -606,12 +674,23 @@ public final Class getLookupType() { */ public final DependencyDescriptor getDependencyDescriptor() { if (this.isField) { - return new LookupDependencyDescriptor((Field) this.member, this.lookupType); + return new ResourceElementResolver.LookupDependencyDescriptor( + (Field) this.member, this.lookupType, isLazyLookup()); } else { - return new LookupDependencyDescriptor((Method) this.member, this.lookupType); + return new ResourceElementResolver.LookupDependencyDescriptor( + (Method) this.member, this.lookupType, isLazyLookup()); } } + + /** + * Determine whether this dependency is marked for lazy lookup. + * The default is {@code false}. + * @since 6.1.2 + */ + boolean isLazyLookup() { + return false; + } } @@ -658,6 +737,11 @@ protected Object getResourceToInject(Object target, @Nullable String requestingB return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : getResource(this, requestingBeanName)); } + + @Override + boolean isLazyLookup() { + return this.lazyLookup; + } } @@ -704,6 +788,11 @@ protected Object getResourceToInject(Object target, @Nullable String requestingB return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : getResource(this, requestingBeanName)); } + + @Override + boolean isLazyLookup() { + return this.lazyLookup; + } } @@ -764,26 +853,142 @@ else if (this.isDefaultName && !StringUtils.hasLength(this.mappedName)) { /** - * Extension of the DependencyDescriptor class, - * overriding the dependency type with the specified resource type. + * {@link BeanRegistrationAotContribution} to inject resources on fields and methods. */ - private static class LookupDependencyDescriptor extends DependencyDescriptor { + private static class AotContribution implements BeanRegistrationAotContribution { - private final Class lookupType; + private static final String REGISTERED_BEAN_PARAMETER = "registeredBean"; - public LookupDependencyDescriptor(Field field, Class lookupType) { - super(field, true); - this.lookupType = lookupType; - } + private static final String INSTANCE_PARAMETER = "instance"; + + private final Class target; + + private final Collection lookupElements; - public LookupDependencyDescriptor(Method method, Class lookupType) { - super(new MethodParameter(method, 0), true); - this.lookupType = lookupType; + @Nullable + private final AutowireCandidateResolver candidateResolver; + + AotContribution(Class target, Collection lookupElements, + @Nullable AutowireCandidateResolver candidateResolver) { + + this.target = target; + this.lookupElements = lookupElements; + this.candidateResolver = candidateResolver; } @Override - public Class getDependencyType() { - return this.lookupType; + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + GeneratedClass generatedClass = generationContext.getGeneratedClasses() + .addForFeatureComponent("ResourceAutowiring", this.target, type -> { + type.addJavadoc("Resource autowiring for {@link $T}.", this.target); + type.addModifiers(javax.lang.model.element.Modifier.PUBLIC); + }); + GeneratedMethod generateMethod = generatedClass.getMethods().add("apply", method -> { + method.addJavadoc("Apply resource autowiring."); + method.addModifiers(javax.lang.model.element.Modifier.PUBLIC, + javax.lang.model.element.Modifier.STATIC); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER); + method.addParameter(this.target, INSTANCE_PARAMETER); + method.returns(this.target); + method.addCode(generateMethodCode(generatedClass.getName(), + generationContext.getRuntimeHints())); + }); + beanRegistrationCode.addInstancePostProcessor(generateMethod.toMethodReference()); + + registerHints(generationContext.getRuntimeHints()); + } + + private CodeBlock generateMethodCode(ClassName targetClassName, RuntimeHints hints) { + CodeBlock.Builder code = CodeBlock.builder(); + for (LookupElement lookupElement : this.lookupElements) { + code.addStatement(generateMethodStatementForElement( + targetClassName, lookupElement, hints)); + } + code.addStatement("return $L", INSTANCE_PARAMETER); + return code.build(); + } + + private CodeBlock generateMethodStatementForElement(ClassName targetClassName, + LookupElement lookupElement, RuntimeHints hints) { + + Member member = lookupElement.getMember(); + if (member instanceof Field field) { + return generateMethodStatementForField( + targetClassName, field, lookupElement, hints); + } + if (member instanceof Method method) { + return generateMethodStatementForMethod( + targetClassName, method, lookupElement, hints); + } + throw new IllegalStateException( + "Unsupported member type " + member.getClass().getName()); + } + + private CodeBlock generateMethodStatementForField(ClassName targetClassName, + Field field, LookupElement lookupElement, RuntimeHints hints) { + + hints.reflection().registerField(field); + CodeBlock resolver = generateFieldResolverCode(field, lookupElement); + AccessControl accessControl = AccessControl.forMember(field); + if (!accessControl.isAccessibleFrom(targetClassName)) { + return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, + REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, + field.getName(), resolver, REGISTERED_BEAN_PARAMETER); + } + + private CodeBlock generateFieldResolverCode(Field field, LookupElement lookupElement) { + if (lookupElement.isDefaultName) { + return CodeBlock.of("$T.$L($S)", ResourceElementResolver.class, + "forField", field.getName()); + } + else { + return CodeBlock.of("$T.$L($S, $S)", ResourceElementResolver.class, + "forField", field.getName(), lookupElement.getName()); + } + } + + private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, + Method method, LookupElement lookupElement, RuntimeHints hints) { + + CodeBlock resolver = generateMethodResolverCode(method, lookupElement); + AccessControl accessControl = AccessControl.forMember(method); + if (!accessControl.isAccessibleFrom(targetClassName)) { + hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, + REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT); + return CodeBlock.of("$L.$L($L.resolve($L))", INSTANCE_PARAMETER, + method.getName(), resolver, REGISTERED_BEAN_PARAMETER); + + } + + private CodeBlock generateMethodResolverCode(Method method, LookupElement lookupElement) { + if (lookupElement.isDefaultName) { + return CodeBlock.of("$T.$L($S, $T.class)", ResourceElementResolver.class, + "forMethod", method.getName(), lookupElement.getLookupType()); + } + else { + return CodeBlock.of("$T.$L($S, $T.class, $S)", ResourceElementResolver.class, + "forMethod", method.getName(), lookupElement.getLookupType(), lookupElement.getName()); + } + } + + private void registerHints(RuntimeHints runtimeHints) { + this.lookupElements.forEach(lookupElement -> + registerProxyIfNecessary(runtimeHints, lookupElement.getDependencyDescriptor())); + } + + private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescriptor dependencyDescriptor) { + if (this.candidateResolver != null) { + Class proxyClass = + this.candidateResolver.getLazyResolutionProxyClass(dependencyDescriptor, null); + if (proxyClass != null) { + ClassHintUtils.registerProxyIfNecessary(proxyClass, runtimeHints); + } + } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java index ffa1f1a06451..0437a3c325e3 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,13 +28,16 @@ import org.springframework.core.type.filter.TypeFilter; /** - * Configures component scanning directives for use with @{@link Configuration} classes. - * Provides support parallel with Spring XML's {@code } element. + * Configures component scanning directives for use with {@link Configuration @Configuration} + * classes. + * + *

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

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

Note that the {@code } element has an * {@code annotation-config} attribute; however, this annotation does not. This is because @@ -46,6 +49,16 @@ * *

See {@link Configuration @Configuration}'s Javadoc for usage examples. * + *

{@code @ComponentScan} can be used as a {@linkplain Repeatable repeatable} + * annotation. {@code @ComponentScan} may also be used as a meta-annotation + * to create custom composed annotations with attribute overrides. + * + *

Locally declared {@code @ComponentScan} annotations always take precedence + * over and effectively hide {@code @ComponentScan} meta-annotations, + * which allows explicit local configuration to override configuration that is + * meta-present (including composed annotations meta-annotated with + * {@code @ComponentScan}). + * * @author Chris Beams * @author Juergen Hoeller * @author Sam Brannen diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java index 535717ed400b..ef45f218b3ff 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -182,17 +182,11 @@ protected void parseScope(Element element, ClassPathBeanDefinitionScanner scanne if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { String mode = element.getAttribute(SCOPED_PROXY_ATTRIBUTE); - if ("targetClass".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); - } - else if ("interfaces".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.INTERFACES); - } - else if ("no".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.NO); - } - else { - throw new IllegalArgumentException("scoped-proxy only supports 'no', 'interfaces' and 'targetClass'"); + switch (mode) { + case "targetClass" -> scanner.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); + case "interfaces" -> scanner.setScopedProxyMode(ScopedProxyMode.INTERFACES); + case "no" -> scanner.setScopedProxyMode(ScopedProxyMode.NO); + default -> throw new IllegalArgumentException("scoped-proxy only supports 'no', 'interfaces' and 'targetClass'"); } } } @@ -234,28 +228,28 @@ protected TypeFilter createTypeFilter(Element element, @Nullable ClassLoader cla String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE); String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE); expression = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(expression); - if ("annotation".equals(filterType)) { - return new AnnotationTypeFilter((Class) ClassUtils.forName(expression, classLoader)); - } - else if ("assignable".equals(filterType)) { - return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader)); - } - else if ("aspectj".equals(filterType)) { - return new AspectJTypeFilter(expression, classLoader); - } - else if ("regex".equals(filterType)) { - return new RegexPatternTypeFilter(Pattern.compile(expression)); - } - else if ("custom".equals(filterType)) { - Class filterClass = ClassUtils.forName(expression, classLoader); - if (!TypeFilter.class.isAssignableFrom(filterClass)) { - throw new IllegalArgumentException( - "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression); + switch (filterType) { + case "annotation" -> { + return new AnnotationTypeFilter((Class) ClassUtils.forName(expression, classLoader)); } - return (TypeFilter) BeanUtils.instantiateClass(filterClass); - } - else { - throw new IllegalArgumentException("Unsupported filter type: " + filterType); + case "assignable" -> { + return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader)); + } + case "aspectj" -> { + return new AspectJTypeFilter(expression, classLoader); + } + case "regex" -> { + return new RegexPatternTypeFilter(Pattern.compile(expression)); + } + case "custom" -> { + Class filterClass = ClassUtils.forName(expression, classLoader); + if (!TypeFilter.class.isAssignableFrom(filterClass)) { + throw new IllegalArgumentException( + "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression); + } + return (TypeFilter) BeanUtils.instantiateClass(filterClass); + } + default -> throw new IllegalArgumentException("Unsupported filter type: " + filterType); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Condition.java b/spring-context/src/main/java/org/springframework/context/annotation/Condition.java index c38fffc33e25..548018566c82 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Condition.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Condition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,19 +20,26 @@ import org.springframework.core.type.AnnotatedTypeMetadata; /** - * A single {@code condition} that must be {@linkplain #matches matched} in order - * for a component to be registered. + * A single condition that must be {@linkplain #matches matched} in order for a + * component to be registered. * - *

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

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

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

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

Multiple conditions on a given class or on a given method will be ordered + * according to the semantics of Spring's {@link org.springframework.core.Ordered} + * interface and {@link org.springframework.core.annotation.Order @Order} annotation. + * See {@link org.springframework.core.annotation.AnnotationAwareOrderComparator} + * for details. + * * @author Phillip Webb + * @author Sam Brannen * @since 4.0 * @see ConfigurationCondition * @see Conditional diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java index 5acfd2d2c16d..b93f1159dd94 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -83,12 +83,13 @@ * *

Via component scanning

* - *

{@code @Configuration} is meta-annotated with {@link Component @Component}, therefore - * {@code @Configuration} classes are candidates for component scanning (typically using - * Spring XML's {@code } element) and therefore may also take + *

Since {@code @Configuration} is meta-annotated with {@link Component @Component}, + * {@code @Configuration} classes are candidates for component scanning — + * for example, using {@link ComponentScan @ComponentScan} or Spring XML's + * {@code } element — and therefore may also take * advantage of {@link Autowired @Autowired}/{@link jakarta.inject.Inject @Inject} - * like any regular {@code @Component}. In particular, if a single constructor is present - * autowiring semantics will be applied transparently for that constructor: + * like any regular {@code @Component}. In particular, if a single constructor is + * present, autowiring semantics will be applied transparently for that constructor: * *

  * @Configuration
@@ -433,6 +434,7 @@
 	 * {@link AnnotationConfigApplicationContext}. If the {@code @Configuration} class
 	 * is registered as a traditional XML bean definition, the name/id of the bean
 	 * element will take precedence.
+	 * 

Alias for {@link Component#value}. * @return the explicit component name, if any (or empty String otherwise) * @see AnnotationBeanNameGenerator */ diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index e462008c7c2f..f952bf666ba1 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,7 +73,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadataReader reader used to parse the underlying {@link Class} * @param beanName must not be {@code null} - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(MetadataReader metadataReader, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -87,10 +86,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if importedBy is not {@code null}). * @param metadataReader reader used to parse the underlying {@link Class} - * @param importedBy the configuration class importing this one or {@code null} + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) { this.metadata = metadataReader.getAnnotationMetadata(); this.resource = metadataReader.getResource(); this.importedBy.add(importedBy); @@ -100,7 +99,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param clazz the underlying {@link Class} to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(Class clazz, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -114,10 +112,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if imported is {@code true}). * @param clazz the underlying {@link Class} to represent - * @param importedBy the configuration class importing this one (or {@code null}) + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(Class clazz, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(Class clazz, ConfigurationClass importedBy) { this.metadata = AnnotationMetadata.introspect(clazz); this.resource = new DescriptiveResource(clazz.getName()); this.importedBy.add(importedBy); @@ -127,7 +125,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadata the metadata for the underlying class to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(AnnotationMetadata metadata, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -149,12 +146,12 @@ String getSimpleName() { return ClassUtils.getShortName(getMetadata().getClassName()); } - void setBeanName(String beanName) { + void setBeanName(@Nullable String beanName) { this.beanName = beanName; } @Nullable - public String getBeanName() { + String getBeanName() { return this.beanName; } @@ -164,7 +161,7 @@ public String getBeanName() { * @since 3.1.1 * @see #getImportedBy() */ - public boolean isImported() { + boolean isImported() { return !this.importedBy.isEmpty(); } @@ -198,6 +195,10 @@ void addImportedResource(String importedResource, Class> getImportedResources() { + return this.importedResources; + } + void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) { this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata); } @@ -206,10 +207,6 @@ Map getImportBeanDefinitionRe return this.importBeanDefinitionRegistrars; } - Map> getImportedResources() { - return this.importedResources; - } - void validate(ProblemReporter problemReporter) { Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index ae75eb1a0178..55d1db9fab90 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -301,8 +301,12 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String } // A bean definition resulting from a component scan can be silently overridden - // by an @Bean method, as of 4.2... - if (existingBeanDef instanceof ScannedGenericBeanDefinition) { + // by an @Bean method - and as of 6.1, even when general overriding is disabled + // as long as the bean class is the same. + if (existingBeanDef instanceof ScannedGenericBeanDefinition scannedBeanDef) { + if (beanMethod.getMetadata().getReturnTypeName().equals(scannedBeanDef.getBeanClassName())) { + this.registry.removeBeanDefinition(beanName); + } return false; } @@ -314,7 +318,7 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String // At this point, it's a top-level override (probably XML), just having been parsed // before configuration class processing kicks in... - if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isAllowBeanDefinitionOverriding()) { + if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) { throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 73a922b7cf8f..21392d5fc7cc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author 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.springframework.aop.scope.ScopedProxyFactoryBean; import org.springframework.asm.Opcodes; import org.springframework.asm.Type; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -37,6 +38,7 @@ import org.springframework.beans.factory.support.SimpleInstantiationStrategy; import org.springframework.cglib.core.ClassGenerator; import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; +import org.springframework.cglib.core.CodeGenerationException; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.CallbackFilter; @@ -47,6 +49,7 @@ import org.springframework.cglib.proxy.NoOp; import org.springframework.cglib.transform.ClassEmitterTransformer; import org.springframework.cglib.transform.TransformingClassGenerator; +import org.springframework.core.SmartClassLoader; import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisException; import org.springframework.objenesis.SpringObjenesis; @@ -106,12 +109,19 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) } return configClass; } - Class enhancedClass = createClass(newEnhancer(configClass, classLoader)); - if (logger.isTraceEnabled()) { - logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", - configClass.getName(), enhancedClass.getName())); + try { + Class enhancedClass = createClass(newEnhancer(configClass, classLoader)); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", + configClass.getName(), enhancedClass.getName())); + } + return enhancedClass; + } + catch (CodeGenerationException ex) { + throw new BeanDefinitionStoreException("Could not enhance configuration class [" + configClass.getName() + + "]. Consider declaring @Configuration(proxyBeanMethods=false) without inter-bean references " + + "between @Bean methods on the configuration class, avoiding the need for CGLIB enhancement.", ex); } - return enhancedClass; } /** @@ -123,13 +133,21 @@ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader cl enhancer.setInterfaces(new Class[] {EnhancedConfiguration.class}); enhancer.setUseFactory(false); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader)); enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); enhancer.setCallbackFilter(CALLBACK_FILTER); enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); return enhancer; } + /** + * Checks whether the given configuration class is reloadable. + */ + private boolean isClassReloadable(Class configSuperClass, @Nullable ClassLoader classLoader) { + return (classLoader instanceof SmartClassLoader smartClassLoader && + smartClassLoader.isClassReloadable(configSuperClass)); + } + /** * Uses enhancer to generate a subclass of superclass, * ensuring that callbacks are registered for the new subclass. @@ -225,7 +243,6 @@ public void end_class() { }; return new TransformingClassGenerator(cg, transformer); } - } @@ -334,6 +351,7 @@ public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); } + @Nullable private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, ConfigurableBeanFactory beanFactory, String beanName) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 59d1f07147b6..9a7917011b7a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -55,6 +55,7 @@ import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; @@ -238,11 +239,18 @@ protected void processConfigurationClass(ConfigurationClass configClass, Predica } // Recursively process the configuration class and its superclass hierarchy. - SourceClass sourceClass = asSourceClass(configClass, filter); - do { - sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter); + SourceClass sourceClass = null; + try { + sourceClass = asSourceClass(configClass, filter); + do { + sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter); + } + while (sourceClass != null); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "I/O failure while processing configuration class [" + sourceClass + "]", ex); } - while (sourceClass != null); this.configurationClasses.put(configClass, configClass); } @@ -267,8 +275,8 @@ protected final SourceClass doProcessConfigurationClass( // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( - sourceClass.getMetadata(), PropertySources.class, - org.springframework.context.annotation.PropertySource.class)) { + sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class, + PropertySources.class, true)) { if (this.propertySourceRegistry != null) { this.propertySourceRegistry.processPropertySource(propertySource); } @@ -278,9 +286,18 @@ protected final SourceClass doProcessConfigurationClass( } } - // Process any @ComponentScan annotations + // Search for locally declared @ComponentScan annotations first. Set componentScans = AnnotationConfigUtils.attributesForRepeatable( - sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); + sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class, + MergedAnnotation::isDirectlyPresent); + + // Fall back to searching for @ComponentScan meta-annotations (which indirectly + // includes locally declared composed annotations). + if (componentScans.isEmpty()) { + componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), + ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent); + } + if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index fd8d0f92a1ea..8068ddd93cbc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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 @@ import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; +import org.springframework.beans.factory.aot.InstanceSupplierCodeGenerator; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; @@ -74,6 +75,7 @@ import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationStartupAware; import org.springframework.context.EnvironmentAware; @@ -90,6 +92,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.io.support.PropertySourceProcessor; +import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; import org.springframework.core.type.AnnotationMetadata; @@ -315,9 +318,8 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe Object configClassAttr = registeredBean.getMergedBeanDefinition() .getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE); if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) { - Class proxyClass = registeredBean.getBeanType().toClass(); return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments -> - new ConfigurationClassProxyBeanRegistrationCodeFragments(codeFragments, proxyClass)); + new ConfigurationClassProxyBeanRegistrationCodeFragments(codeFragments, registeredBean)); } return null; } @@ -386,11 +388,11 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. }); // Detect any custom bean name generation strategy supplied through the enclosing application context - SingletonBeanRegistry sbr = null; - if (registry instanceof SingletonBeanRegistry _sbr) { - sbr = _sbr; + SingletonBeanRegistry singletonRegistry = null; + if (registry instanceof SingletonBeanRegistry sbr) { + singletonRegistry = sbr; if (!this.localBeanNameGeneratorSet) { - BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton( + BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton( AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); if (generator != null) { this.componentScanBeanNameGenerator = generator; @@ -451,8 +453,8 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. while (!candidates.isEmpty()); // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes - if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { - sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); + if (singletonRegistry != null && !singletonRegistry.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { + singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } // Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary @@ -506,11 +508,12 @@ public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFact throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" + beanName + "' since it is not stored in an AbstractBeanDefinition subclass"); } - else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) { - logger.info("Cannot enhance @Configuration bean definition '" + beanName + + else if (logger.isWarnEnabled() && beanFactory.containsSingleton(beanName)) { + logger.warn("Cannot enhance @Configuration bean definition '" + beanName + "' since its singleton instance has been created too early. The typical cause " + "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + - "return type: Consider declaring such methods as 'static'."); + "return type: Consider declaring such methods as 'static' and/or marking the " + + "containing configuration class as 'proxyBeanMethods=false'."); } configBeanDefs.put(beanName, abd); } @@ -550,6 +553,7 @@ public ImportAwareBeanPostProcessor(BeanFactory beanFactory) { } @Override + @Nullable public PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) { // Inject the BeanFactory before AutowiredAnnotationBeanPostProcessor's // postProcessProperties method attempts to autowire other configuration beans. @@ -645,15 +649,17 @@ private Map buildImportAwareMappings() { } return mappings; } - } + private static class PropertySourcesAotContribution implements BeanFactoryInitializationAotContribution { private static final String ENVIRONMENT_VARIABLE = "environment"; private static final String RESOURCE_LOADER_VARIABLE = "resourceLoader"; + private final Log logger = LogFactory.getLog(getClass()); + private final List descriptors; private final Function resourceResolver; @@ -673,14 +679,28 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati private void registerRuntimeHints(RuntimeHints hints) { for (PropertySourceDescriptor descriptor : this.descriptors) { - Class factory = descriptor.propertySourceFactory(); - if (factory != null) { - hints.reflection().registerType(factory, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + Class factoryClass = descriptor.propertySourceFactory(); + if (factoryClass != null) { + hints.reflection().registerType(factoryClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } for (String location : descriptor.locations()) { - Resource resource = this.resourceResolver.apply(location); - if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) { - hints.resources().registerPattern(classPathResource.getPath()); + if (location.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX) || + (location.startsWith(ResourcePatternResolver.CLASSPATH_URL_PREFIX) && + (location.contains("*") || location.contains("?")))) { + + if (logger.isWarnEnabled()) { + logger.warn(""" + Runtime hint registration is not supported for the 'classpath*:' \ + prefix or wildcards in @PropertySource locations. Please manually \ + register a resource hint for each property source location represented \ + by '%s'.""".formatted(location)); + } + } + else { + Resource resource = this.resourceResolver.apply(location); + if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) { + hints.resources().registerPattern(classPathResource.getPath()); + } } } } @@ -736,29 +756,30 @@ private CodeBlock generatePropertySourceDescriptorCode(PropertySourceDescriptor } private CodeBlock handleNull(@Nullable Object value, Supplier nonNull) { - if (value == null) { - return CodeBlock.of("null"); - } - else { - return nonNull.get(); - } + return (value == null ? CodeBlock.of("null") : nonNull.get()); } - } + private static class ConfigurationClassProxyBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { + private final RegisteredBean registeredBean; + private final Class proxyClass; - public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, - Class proxyClass) { + public ConfigurationClassProxyBeanRegistrationCodeFragments( + BeanRegistrationCodeFragments codeFragments, RegisteredBean registeredBean) { + super(codeFragments); - this.proxyClass = proxyClass; + this.registeredBean = registeredBean; + this.proxyClass = registeredBean.getBeanType().toClass(); } @Override - public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate attributeFilter) { + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { + CodeBlock.Builder code = CodeBlock.builder(); code.add(super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition, attributeFilter)); @@ -768,27 +789,34 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { - Executable executableToUse = proxyExecutable(generationContext.getRuntimeHints(), constructorOrFactoryMethod); - return super.generateInstanceSupplierCode(generationContext, beanRegistrationCode, - executableToUse, allowDirectSupplierShortcut); + + InstantiationDescriptor instantiationDescriptor = proxyInstantiationDescriptor( + generationContext.getRuntimeHints(), this.registeredBean.resolveInstantiationDescriptor()); + + return new InstanceSupplierCodeGenerator(generationContext, + beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut) + .generateCode(this.registeredBean, instantiationDescriptor); } - private Executable proxyExecutable(RuntimeHints runtimeHints, Executable userExecutable) { + private InstantiationDescriptor proxyInstantiationDescriptor( + RuntimeHints runtimeHints, InstantiationDescriptor instantiationDescriptor) { + + Executable userExecutable = instantiationDescriptor.executable(); if (userExecutable instanceof Constructor userConstructor) { try { runtimeHints.reflection().registerConstructor(userConstructor, ExecutableMode.INTROSPECT); - return this.proxyClass.getConstructor(userExecutable.getParameterTypes()); + Constructor constructor = this.proxyClass.getConstructor(userExecutable.getParameterTypes()); + return new InstantiationDescriptor(constructor); } catch (NoSuchMethodException ex) { throw new IllegalStateException("No matching constructor found on proxy " + this.proxyClass, ex); } } - return userExecutable; + return instantiationDescriptor; } - } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java index f50fdff6a1e8..1718ffd4de49 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -85,7 +85,7 @@ protected Object buildLazyResolutionProxy(DependencyDescriptor descriptor, @Null } private Object buildLazyResolutionProxy( - final DependencyDescriptor descriptor, final @Nullable String beanName, boolean classOnly) { + final DependencyDescriptor descriptor, @Nullable final String beanName, boolean classOnly) { BeanFactory beanFactory = getBeanFactory(); Assert.state(beanFactory instanceof DefaultListableBeanFactory, @@ -98,10 +98,6 @@ public Class getTargetClass() { return descriptor.getDependencyType(); } @Override - public boolean isStatic() { - return false; - } - @Override public Object getTarget() { Set autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null); Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null); @@ -128,9 +124,6 @@ else if (Set.class == type || Collection.class == type) { } return target; } - @Override - public void releaseTarget(Object target) { - } }; ProxyFactory pf = new ProxyFactory(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java b/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java index e31c02d0c670..506a45c6607c 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -165,7 +165,7 @@ enum AspectJWeaving { * is present in the classpath. If there is no such resource, then AspectJ * load-time weaving will be switched off. */ - AUTODETECT; + AUTODETECT } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java index bc3d4992c542..184872837729 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,6 @@ * public MyService myService() { * return new MyService(); * } - * * }

* *

If the configuration class above is processed, {@code MyHints} will be diff --git a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java index 477de756a8c7..86b1ede8f709 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,20 +94,20 @@ public LoadTimeWeaver loadTimeWeaver() { if (this.enableLTW != null) { AspectJWeaving aspectJWeaving = this.enableLTW.getEnum("aspectjWeaving"); switch (aspectJWeaving) { - case DISABLED: + case DISABLED -> { // AJ weaving is disabled -> do nothing - break; - case AUTODETECT: + } + case AUTODETECT -> { if (this.beanClassLoader.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) == null) { // No aop.xml present on the classpath -> treat as 'disabled' break; } // aop.xml is present on the classpath -> enable AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); - break; - case ENABLED: + } + case ENABLED -> { AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); - break; + } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java index 3832996e448d..2183d3d6e1c3 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,6 +81,7 @@ * @see Bean * @see ComponentScan * @see org.springframework.stereotype.Component + * @see org.springframework.beans.factory.config.BeanDefinition#setPrimary */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java index deedc4cb0071..cc9b664921de 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.context.annotation; -import org.springframework.core.env.Profiles; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.MultiValueMap; @@ -36,7 +35,7 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { - if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) { + if (context.getEnvironment().matchesProfiles((String[]) value)) { return true; } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java index a112a3c9e43b..d6e5dc4339ab 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java @@ -147,11 +147,9 @@ * ConfigurableEnvironment} and {@link org.springframework.core.env.MutablePropertySources * MutablePropertySources} javadocs for details. * - *

NOTE: This annotation is repeatable according to Java 8 conventions. - * However, all such {@code @PropertySource} annotations need to be declared at the same - * level: either directly on the configuration class or as meta-annotations on the - * same custom annotation. Mixing direct annotations and meta-annotations is not - * recommended since direct annotations will effectively override meta-annotations. + *

{@code @PropertySource} can be used as a {@linkplain Repeatable repeatable} + * annotation. {@code @PropertySource} may also be used as a meta-annotation + * to create custom composed annotations with attribute overrides. * * @author Chris Beams * @author Juergen Hoeller @@ -203,13 +201,14 @@ *

The default {@link #factory() factory} supports both traditional and * XML-based properties file formats — for example, * {@code "classpath:/com/myco/app.properties"} or {@code "file:/path/to/file.xml"}. - *

Resource location wildcards (e.g. **/*.properties) are not permitted; - * each location must evaluate to exactly one resource. - *

${...} placeholders will be resolved against property sources already + *

As of Spring Framework 6.1, resource location wildcards are also + * supported — for example, {@code "classpath*:/config/*.properties"}. + *

{@code ${...}} placeholders will be resolved against property sources already * registered with the {@code Environment}. See {@linkplain PropertySource above} * for examples. *

Each location will be added to the enclosing {@code Environment} as its own - * property source, and in the order declared. + * property source, and in the order declared (or in the order in which resource + * locations are resolved when location wildcards are used). */ String[] value(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java new file mode 100644 index 000000000000..d3a78ea91b52 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java @@ -0,0 +1,330 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Resolver for the injection of named beans on a field or method element, + * following the rules of the {@link jakarta.annotation.Resource} annotation + * but without any JNDI support. This is primarily intended for AOT processing. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 6.1.2 + * @see CommonAnnotationBeanPostProcessor + * @see jakarta.annotation.Resource + */ +public abstract class ResourceElementResolver { + + private final String name; + + private final boolean defaultName; + + + ResourceElementResolver(String name, boolean defaultName) { + this.name = name; + this.defaultName = defaultName; + } + + + /** + * Create a new {@link ResourceFieldResolver} for the specified field. + * @param fieldName the field name + * @return a new {@link ResourceFieldResolver} instance + */ + public static ResourceElementResolver forField(String fieldName) { + return new ResourceFieldResolver(fieldName, true, fieldName); + } + + /** + * Create a new {@link ResourceFieldResolver} for the specified field and resource name. + * @param fieldName the field name + * @param resourceName the resource name + * @return a new {@link ResourceFieldResolver} instance + */ + public static ResourceElementResolver forField(String fieldName, String resourceName) { + return new ResourceFieldResolver(resourceName, false, fieldName); + } + + /** + * Create a new {@link ResourceMethodResolver} for the specified method + * using a resource name that infers from the method name. + * @param methodName the method name + * @param parameterType the parameter type. + * @return a new {@link ResourceMethodResolver} instance + */ + public static ResourceElementResolver forMethod(String methodName, Class parameterType) { + return new ResourceMethodResolver(defaultResourceNameForMethod(methodName), true, + methodName, parameterType); + } + + /** + * Create a new {@link ResourceMethodResolver} for the specified method + * and resource name. + * @param methodName the method name + * @param parameterType the parameter type + * @param resourceName the resource name + * @return a new {@link ResourceMethodResolver} instance + */ + public static ResourceElementResolver forMethod(String methodName, Class parameterType, String resourceName) { + return new ResourceMethodResolver(resourceName, false, methodName, parameterType); + } + + private static String defaultResourceNameForMethod(String methodName) { + if (methodName.startsWith("set") && methodName.length() > 3) { + return StringUtils.uncapitalizeAsProperty(methodName.substring(3)); + } + return methodName; + } + + + /** + * Resolve the value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field or method parameter value + */ + @Nullable + @SuppressWarnings("unchecked") + public T resolve(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + return (T) (isLazyLookup(registeredBean) ? buildLazyResourceProxy(registeredBean) : + resolveValue(registeredBean)); + } + + /** + * Resolve the value for the specified registered bean and set it using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public abstract void resolveAndSet(RegisteredBean registeredBean, Object instance); + + /** + * Create a suitable {@link DependencyDescriptor} for the specified bean. + * @param registeredBean the registered bean + * @return a descriptor for that bean + */ + abstract DependencyDescriptor createDependencyDescriptor(RegisteredBean registeredBean); + + abstract Class getLookupType(RegisteredBean registeredBean); + + abstract AnnotatedElement getAnnotatedElement(RegisteredBean registeredBean); + + boolean isLazyLookup(RegisteredBean registeredBean) { + AnnotatedElement ae = getAnnotatedElement(registeredBean); + Lazy lazy = ae.getAnnotation(Lazy.class); + return (lazy != null && lazy.value()); + } + + private Object buildLazyResourceProxy(RegisteredBean registeredBean) { + Class lookupType = getLookupType(registeredBean); + + TargetSource ts = new TargetSource() { + @Override + public Class getTargetClass() { + return lookupType; + } + @Override + public Object getTarget() { + return resolveValue(registeredBean); + } + }; + + ProxyFactory pf = new ProxyFactory(); + pf.setTargetSource(ts); + if (lookupType.isInterface()) { + pf.addInterface(lookupType); + } + return pf.getProxy(registeredBean.getBeanFactory().getBeanClassLoader()); + } + + /** + * Resolve the value to inject for this instance. + * @param registeredBean the bean registration + * @return the value to inject + */ + private Object resolveValue(RegisteredBean registeredBean) { + ConfigurableListableBeanFactory factory = registeredBean.getBeanFactory(); + + Object resource; + Set autowiredBeanNames; + DependencyDescriptor descriptor = createDependencyDescriptor(registeredBean); + if (this.defaultName && !factory.containsBean(this.name)) { + autowiredBeanNames = new LinkedHashSet<>(); + resource = factory.resolveDependency(descriptor, registeredBean.getBeanName(), autowiredBeanNames, null); + if (resource == null) { + throw new NoSuchBeanDefinitionException(descriptor.getDependencyType(), "No resolvable resource object"); + } + } + else { + resource = factory.resolveBeanByName(this.name, descriptor); + autowiredBeanNames = Collections.singleton(this.name); + } + + for (String autowiredBeanName : autowiredBeanNames) { + if (factory.containsBean(autowiredBeanName)) { + factory.registerDependentBean(autowiredBeanName, registeredBean.getBeanName()); + } + } + return resource; + } + + + private static final class ResourceFieldResolver extends ResourceElementResolver { + + private final String fieldName; + + public ResourceFieldResolver(String name, boolean defaultName, String fieldName) { + super(name, defaultName); + this.fieldName = fieldName; + } + + @Override + public void resolveAndSet(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Field field = getField(registeredBean); + Object resolved = resolve(registeredBean); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, instance, resolved); + } + + @Override + protected DependencyDescriptor createDependencyDescriptor(RegisteredBean registeredBean) { + Field field = getField(registeredBean); + return new LookupDependencyDescriptor(field, field.getType(), isLazyLookup(registeredBean)); + } + + @Override + protected Class getLookupType(RegisteredBean registeredBean) { + return getField(registeredBean).getType(); + } + + @Override + protected AnnotatedElement getAnnotatedElement(RegisteredBean registeredBean) { + return getField(registeredBean); + } + + private Field getField(RegisteredBean registeredBean) { + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), this.fieldName); + Assert.notNull(field, + () -> "No field '" + this.fieldName + "' found on " + registeredBean.getBeanClass().getName()); + return field; + } + } + + + private static final class ResourceMethodResolver extends ResourceElementResolver { + + private final String methodName; + + private final Class lookupType; + + private ResourceMethodResolver(String name, boolean defaultName, String methodName, Class lookupType) { + super(name, defaultName); + this.methodName = methodName; + this.lookupType = lookupType; + } + + @Override + public void resolveAndSet(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Method method = getMethod(registeredBean); + Object resolved = resolve(registeredBean); + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, instance, resolved); + } + + @Override + protected DependencyDescriptor createDependencyDescriptor(RegisteredBean registeredBean) { + return new LookupDependencyDescriptor( + getMethod(registeredBean), this.lookupType, isLazyLookup(registeredBean)); + } + + @Override + protected Class getLookupType(RegisteredBean bean) { + return this.lookupType; + } + + @Override + protected AnnotatedElement getAnnotatedElement(RegisteredBean registeredBean) { + return getMethod(registeredBean); + } + + private Method getMethod(RegisteredBean registeredBean) { + Method method = ReflectionUtils.findMethod(registeredBean.getBeanClass(), this.methodName, this.lookupType); + Assert.notNull(method, + () -> "Method '%s' with parameter type '%s' declared on %s could not be found.".formatted( + this.methodName, this.lookupType.getName(), registeredBean.getBeanClass().getName())); + return method; + } + } + + + /** + * Extension of the DependencyDescriptor class, + * overriding the dependency type with the specified resource type. + */ + @SuppressWarnings("serial") + static class LookupDependencyDescriptor extends DependencyDescriptor { + + private final Class lookupType; + + private final boolean lazyLookup; + + public LookupDependencyDescriptor(Field field, Class lookupType, boolean lazyLookup) { + super(field, true); + this.lookupType = lookupType; + this.lazyLookup = lazyLookup; + } + + public LookupDependencyDescriptor(Method method, Class lookupType, boolean lazyLookup) { + super(new MethodParameter(method, 0), true); + this.lookupType = lookupType; + this.lazyLookup = lazyLookup; + } + + @Override + public Class getDependencyType() { + return this.lookupType; + } + + @Override + public boolean supportsLazyResolution() { + return !this.lazyLookup; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java index 9acfa463e246..b342e4d789ee 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -257,6 +257,7 @@ public Builder classOutput(Path classOutput) { * @return this builder for method chaining */ public Builder groupId(String groupId) { + Assert.hasText(groupId, "'groupId' must not be empty"); this.groupId = groupId; return this; } @@ -268,6 +269,7 @@ public Builder groupId(String groupId) { * @return this builder for method chaining */ public Builder artifactId(String artifactId) { + Assert.hasText(artifactId, "'artifactId' must not be empty"); this.artifactId = artifactId; return this; } @@ -279,14 +281,12 @@ public Settings build() { Assert.notNull(this.sourceOutput, "'sourceOutput' must not be null"); Assert.notNull(this.resourceOutput, "'resourceOutput' must not be null"); Assert.notNull(this.classOutput, "'classOutput' must not be null"); - Assert.hasText(this.groupId, "'groupId' must not be null or empty"); - Assert.hasText(this.artifactId, "'artifactId' must not be null or empty"); + Assert.notNull(this.groupId, "'groupId' must not be null"); + Assert.notNull(this.artifactId, "'artifactId' must not be null"); return new Settings(this.sourceOutput, this.resourceOutput, this.classOutput, this.groupId, this.artifactId); } - } - } } diff --git a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java index 7e97b686254f..0331b145c10e 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,9 +52,9 @@ public ClassName processAheadOfTime(GenericApplicationContext applicationContext GenerationContext generationContext) { return withCglibClassHandler(new CglibClassHandler(generationContext), () -> { applicationContext.refreshForAotProcessing(generationContext.getRuntimeHints()); - DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); ApplicationContextInitializationCodeGenerator codeGenerator = - new ApplicationContextInitializationCodeGenerator(generationContext); + new ApplicationContextInitializationCodeGenerator(applicationContext, generationContext); + DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator); return codeGenerator.getGeneratedClass().getName(); }); diff --git a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java index 3a7253b0440a..5305508b9da9 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.context.aot; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -49,6 +50,7 @@ * Internal code generator to create the {@link ApplicationContextInitializer}. * * @author Phillip Webb + * @author Stephane Nicoll * @since 6.0 */ class ApplicationContextInitializationCodeGenerator implements BeanFactoryInitializationCode { @@ -57,12 +59,15 @@ class ApplicationContextInitializationCodeGenerator implements BeanFactoryInitia private static final String APPLICATION_CONTEXT_VARIABLE = "applicationContext"; - private final List initializers = new ArrayList<>(); + private final GenericApplicationContext applicationContext; private final GeneratedClass generatedClass; + private final List initializers = new ArrayList<>(); + - ApplicationContextInitializationCodeGenerator(GenerationContext generationContext) { + ApplicationContextInitializationCodeGenerator(GenericApplicationContext applicationContext, GenerationContext generationContext) { + this.applicationContext = applicationContext; this.generatedClass = generationContext.getGeneratedClasses() .addForFeature("ApplicationContextInitializer", this::generateType); this.generatedClass.reserveMethodNames(INITIALIZE_METHOD); @@ -97,6 +102,7 @@ private CodeBlock generateInitializeCode() { BEAN_FACTORY_VARIABLE, ContextAnnotationAutowireCandidateResolver.class); code.addStatement("$L.setDependencyComparator($T.INSTANCE)", BEAN_FACTORY_VARIABLE, AnnotationAwareOrderComparator.class); + code.add(generateActiveProfilesInitializeCode()); ArgumentCodeGenerator argCodeGenerator = createInitializerMethodArgumentCodeGenerator(); for (MethodReference initializer : this.initializers) { code.addStatement(initializer.toInvokeCodeBlock(argCodeGenerator, this.generatedClass.getName())); @@ -104,6 +110,17 @@ private CodeBlock generateInitializeCode() { return code.build(); } + private CodeBlock generateActiveProfilesInitializeCode() { + CodeBlock.Builder code = CodeBlock.builder(); + ConfigurableEnvironment environment = this.applicationContext.getEnvironment(); + if (!Arrays.equals(environment.getActiveProfiles(), environment.getDefaultProfiles())) { + for (String activeProfile : environment.getActiveProfiles()) { + code.addStatement("$L.getEnvironment().addActiveProfile($S)", APPLICATION_CONTEXT_VARIABLE, activeProfile); + } + } + return code.build(); + } + static ArgumentCodeGenerator createInitializerMethodArgumentCodeGenerator() { return ArgumentCodeGenerator.from(new InitializerMethodArgumentCodeGenerator()); } diff --git a/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java index ca616b8e5744..e8804416eeeb 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,21 +35,22 @@ */ class KotlinReflectionBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { - @Nullable @Override + @Nullable public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (KotlinDetector.isKotlinType(beanClass)) { - return new KotlinReflectionBeanRegistrationAotContribution(beanClass); + return new AotContribution(beanClass); } return null; } - private static class KotlinReflectionBeanRegistrationAotContribution implements BeanRegistrationAotContribution { + + private static class AotContribution implements BeanRegistrationAotContribution { private final Class beanClass; - public KotlinReflectionBeanRegistrationAotContribution(Class beanClass) { + public AotContribution(Class beanClass) { this.beanClass = beanClass; } @@ -66,6 +67,10 @@ private void registerHints(Class type, RuntimeHints runtimeHints) { if (superClass != null) { registerHints(superClass, runtimeHints); } + Class enclosingClass = type.getEnclosingClass(); + if (enclosingClass != null) { + runtimeHints.reflection().registerType(enclosingClass); + } } } diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java index 238350ffc226..9cba020aef54 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,8 @@ */ class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final ReflectiveRuntimeHintsRegistrar REGISTRAR = new ReflectiveRuntimeHintsRegistrar(); + private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); + @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { @@ -49,7 +50,9 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL return new ReflectiveProcessorBeanFactoryInitializationAotContribution(beanTypes); } - private static class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution { + + private static class ReflectiveProcessorBeanFactoryInitializationAotContribution + implements BeanFactoryInitializationAotContribution { private final Class[] types; @@ -60,9 +63,8 @@ public ReflectiveProcessorBeanFactoryInitializationAotContribution(Class[] ty @Override public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - REGISTRAR.registerRuntimeHints(runtimeHints, this.types); + registrar.registerRuntimeHints(runtimeHints, this.types); } - } } diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index 43b6ddd89d7f..76be0c052529 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -21,6 +21,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; @@ -262,6 +263,24 @@ private Collection> retrieveApplicationListeners( if (supportsEvent(beanFactory, listenerBeanName, eventType)) { ApplicationListener listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class); + + // Despite best efforts to avoid it, unwrapped proxies (singleton targets) can end up in the + // list of programmatically registered listeners. In order to avoid duplicates, we need to find + // and replace them by their proxy counterparts, because if both a proxy and its target end up + // in 'allListeners', listeners will fire twice. + ApplicationListener unwrappedListener = + (ApplicationListener) AopProxyUtils.getSingletonTarget(listener); + if (listener != unwrappedListener) { + if (filteredListeners != null && filteredListeners.contains(unwrappedListener)) { + filteredListeners.remove(unwrappedListener); + filteredListeners.add(listener); + } + if (allListeners.contains(unwrappedListener)) { + allListeners.remove(unwrappedListener); + allListeners.add(listener); + } + } + if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) { if (retriever != null) { if (beanFactory.isSingleton(listenerBeanName)) { @@ -404,7 +423,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return this.eventType.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.sourceType); + return Objects.hash(this.eventType, this.sourceType); } @Override diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java index fab9067b20d6..0823d051c340 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ public abstract class ApplicationContextEvent extends ApplicationEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ApplicationContextEvent}. * @param source the {@code ApplicationContext} that the event is raised for * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java index 4fff846e8c96..a57899969d7f 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,7 +110,10 @@ public interface ApplicationEventMulticaster { * Multicast the given application event to appropriate listeners. *

Consider using {@link #multicastEvent(ApplicationEvent, ResolvableType)} * if possible as it provides better support for generics-based events. + *

If a matching {@code ApplicationListener} does not support asynchronous + * execution, it must be run within the calling thread of this multicast call. * @param event the event to multicast + * @see ApplicationListener#supportsAsyncExecution() */ void multicastEvent(ApplicationEvent event); @@ -118,9 +121,12 @@ public interface ApplicationEventMulticaster { * Multicast the given application event to appropriate listeners. *

If the {@code eventType} is {@code null}, a default type is built * based on the {@code event} instance. + *

If a matching {@code ApplicationListener} does not support asynchronous + * execution, it must be run within the calling thread of this multicast call. * @param event the event to multicast * @param eventType the type of event (can be {@code null}) * @since 4.2 + * @see ApplicationListener#supportsAsyncExecution() */ void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType); diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index 927231aab7ce..1c86b5646c4e 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,8 @@ import org.springframework.context.PayloadApplicationEvent; import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.CoroutinesUtils; +import org.springframework.core.KotlinDetector; import org.springframework.core.Ordered; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; @@ -64,6 +66,7 @@ * @author Stephane Nicoll * @author Juergen Hoeller * @author Sam Brannen + * @author Sebastien Deleuze * @since 4.2 */ public class ApplicationListenerMethodAdapter implements GenericApplicationListener { @@ -121,7 +124,7 @@ public ApplicationListenerMethodAdapter(String beanName, Class targetClass, M } private static List resolveDeclaredEventTypes(Method method, @Nullable EventListener ann) { - int count = method.getParameterCount(); + int count = (KotlinDetector.isSuspendingFunction(method) ? method.getParameterCount() - 1 : method.getParameterCount()); if (count > 1) { throw new IllegalStateException( "Maximum one parameter is allowed for event listener method: " + method); @@ -219,13 +222,14 @@ protected String getDefaultListenerId() { for (Class paramType : method.getParameterTypes()) { sj.add(paramType.getName()); } - return ClassUtils.getQualifiedMethodName(method) + sj.toString(); + return ClassUtils.getQualifiedMethodName(method) + sj; } /** * Process the specified {@link ApplicationEvent}, checking if the condition * matches and handling a non-null result, if any. + * @param event the event to process through the listener method */ public void processEvent(ApplicationEvent event) { Object[] args = resolveArguments(event); @@ -240,6 +244,29 @@ public void processEvent(ApplicationEvent event) { } } + /** + * Determine whether the listener method would actually handle the given + * event, checking if the condition matches. + * @param event the event to process through the listener method + * @since 6.1 + */ + public boolean shouldHandle(ApplicationEvent event) { + return shouldHandle(event, resolveArguments(event)); + } + + private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) { + if (args == null) { + return false; + } + String condition = getCondition(); + if (StringUtils.hasText(condition)) { + Assert.notNull(this.evaluator, "EventExpressionEvaluator must not be null"); + return this.evaluator.condition( + condition, event, this.targetMethod, this.methodKey, args); + } + return true; + } + /** * Resolve the method arguments to use for the specified {@link ApplicationEvent}. *

These arguments will be used to invoke the method handled by this instance. @@ -291,8 +318,8 @@ else if (result instanceof org.springframework.util.concurrent.ListenableFuture< } } - private void publishEvents(Object result) { - if (result.getClass().isArray()) { + private void publishEvents(@Nullable Object result) { + if (result != null && result.getClass().isArray()) { Object[] events = ObjectUtils.toObjectArray(result); for (Object event : events) { publishEvent(event); @@ -319,24 +346,11 @@ protected void handleAsyncError(Throwable t) { logger.error("Unexpected error occurred in asynchronous listener", t); } - private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) { - if (args == null) { - return false; - } - String condition = getCondition(); - if (StringUtils.hasText(condition)) { - Assert.notNull(this.evaluator, "EventExpressionEvaluator must not be null"); - return this.evaluator.condition( - condition, event, this.targetMethod, this.methodKey, args, this.applicationContext); - } - return true; - } - /** * Invoke the event listener method with the given argument values. */ @Nullable - protected Object doInvoke(Object... args) { + protected Object doInvoke(@Nullable Object... args) { Object bean = getTargetBean(); // Detect package-protected NullBean instance through equals(null) check if (bean.equals(null)) { @@ -345,6 +359,9 @@ protected Object doInvoke(Object... args) { ReflectionUtils.makeAccessible(this.method); try { + if (KotlinDetector.isSuspendingFunction(this.method)) { + return CoroutinesUtils.invokeSuspendingFunction(this.method, bean, args); + } return this.method.invoke(bean, args); } catch (IllegalArgumentException ex) { @@ -399,8 +416,8 @@ protected String getCondition() { * the given error message. * @param message error message to append the HandlerMethod details to */ - protected String getDetailedErrorMessage(Object bean, String message) { - StringBuilder sb = new StringBuilder(message).append('\n'); + protected String getDetailedErrorMessage(Object bean, @Nullable String message) { + StringBuilder sb = (StringUtils.hasLength(message) ? new StringBuilder(message).append('\n') : new StringBuilder()); sb.append("HandlerMethod details: \n"); sb.append("Bean [").append(bean.getClass().getName()).append("]\n"); sb.append("Method [").append(this.method.toGenericString()).append("]\n"); @@ -414,7 +431,7 @@ protected String getDetailedErrorMessage(Object bean, String message) { * beans, and others). Event listener beans that require proxying should prefer * class-based proxy mechanisms. */ - private void assertTargetBean(Method method, Object targetBean, Object[] args) { + private void assertTargetBean(Method method, Object targetBean, @Nullable Object[] args) { Class methodDeclaringClass = method.getDeclaringClass(); Class targetBeanClass = targetBean.getClass(); if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { @@ -426,7 +443,7 @@ private void assertTargetBean(Method method, Object targetBean, Object[] args) { } } - private String getInvocationErrorMessage(Object bean, String message, Object[] resolvedArgs) { + private String getInvocationErrorMessage(Object bean, @Nullable String message, @Nullable Object[] resolvedArgs) { StringBuilder sb = new StringBuilder(getDetailedErrorMessage(bean, message)); sb.append("Resolved arguments: \n"); for (int i = 0; i < resolvedArgs.length; i++) { diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java index 900bf30e49ca..8d0e2e56541c 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ public class ContextClosedEvent extends ApplicationContextEvent { /** - * Creates a new ContextClosedEvent. + * Create a new {@code ContextClosedEvent}. * @param source the {@code ApplicationContext} that has been closed * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java index 27c657a948e6..ba55c6a56c27 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ public class ContextRefreshedEvent extends ApplicationContextEvent { /** - * Create a new ContextRefreshedEvent. + * Create a new {@code ContextRefreshedEvent}. * @param source the {@code ApplicationContext} that has been initialized * or refreshed (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java index bfd615d5c120..f0cf6d6bb0d4 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ public class ContextStartedEvent extends ApplicationContextEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ContextStartedEvent}. * @param source the {@code ApplicationContext} that has been started * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java index 4a156b207b8c..791e08c282c2 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ public class ContextStoppedEvent extends ApplicationContextEvent { /** - * Create a new ContextStoppedEvent. + * Create a new {@code ContextStoppedEvent}. * @param source the {@code ApplicationContext} that has been stopped * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java index ed1974f47a06..acef8846be64 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +20,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.expression.AnnotatedElementKey; -import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.CachedExpressionEvaluator; import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; +import org.springframework.expression.spel.support.StandardEvaluationContext; /** * Utility class for handling SpEL expression parsing for application events. @@ -41,23 +40,32 @@ class EventExpressionEvaluator extends CachedExpressionEvaluator { private final Map conditionCache = new ConcurrentHashMap<>(64); + private final StandardEvaluationContext originalEvaluationContext; + + EventExpressionEvaluator(StandardEvaluationContext originalEvaluationContext) { + this.originalEvaluationContext = originalEvaluationContext; + } /** * Determine if the condition defined by the specified expression evaluates * to {@code true}. */ public boolean condition(String conditionExpression, ApplicationEvent event, Method targetMethod, - AnnotatedElementKey methodKey, Object[] args, @Nullable BeanFactory beanFactory) { - - EventExpressionRootObject root = new EventExpressionRootObject(event, args); - MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext( - root, targetMethod, args, getParameterNameDiscoverer()); - if (beanFactory != null) { - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); - } + AnnotatedElementKey methodKey, Object[] args) { + EventExpressionRootObject rootObject = new EventExpressionRootObject(event, args); + EvaluationContext evaluationContext = createEvaluationContext(rootObject, targetMethod, args); return (Boolean.TRUE.equals(getExpression(this.conditionCache, methodKey, conditionExpression).getValue( evaluationContext, Boolean.class))); } + private EvaluationContext createEvaluationContext(EventExpressionRootObject rootObject, + Method method, Object[] args) { + + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(rootObject, + method, args, getParameterNameDiscoverer()); + this.originalEvaluationContext.applyDelegatesTo(evaluationContext); + return evaluationContext; + } + } diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java index 1fea6e198da0..4e16ecadfabb 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -39,10 +39,12 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.MethodIntrospector; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -75,6 +77,8 @@ public class EventListenerMethodProcessor @Nullable private List eventListenerFactories; + private final StandardEvaluationContext originalEvaluationContext; + @Nullable private final EventExpressionEvaluator evaluator; @@ -82,7 +86,8 @@ public class EventListenerMethodProcessor public EventListenerMethodProcessor() { - this.evaluator = new EventExpressionEvaluator(); + this.originalEvaluationContext = new StandardEvaluationContext(); + this.evaluator = new EventExpressionEvaluator(this.originalEvaluationContext); } @Override @@ -95,6 +100,7 @@ public void setApplicationContext(ApplicationContext applicationContext) { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { this.beanFactory = beanFactory; + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); Map beans = beanFactory.getBeansOfType(EventListenerFactory.class, false, false); List factories = new ArrayList<>(beans.values()); diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java index 763f96f533af..21e8b79da29a 100644 --- a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.context.event; +import java.util.function.Consumer; + import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; @@ -53,4 +55,17 @@ default boolean supportsEventType(Class eventType) { */ boolean supportsEventType(ResolvableType eventType); + + /** + * Create a new {@code ApplicationListener} for the given event type. + * @param eventType the event to listen to + * @param consumer the consumer to invoke when a matching event is fired + * @param the specific {@code ApplicationEvent} subclass to listen to + * @return a corresponding {@code ApplicationListener} instance + * @since 6.1.3 + */ + static GenericApplicationListener forEventType(Class eventType, Consumer consumer) { + return new GenericApplicationListenerDelegate<>(eventType, consumer); + } + } diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java new file mode 100644 index 000000000000..9b2adac9c30e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.util.function.Consumer; + +import org.springframework.context.ApplicationEvent; +import org.springframework.core.ResolvableType; + +/** + * A {@link GenericApplicationListener} implementation that supports a single event type. + * + * @author Stephane Nicoll + * @since 6.1.3 + * @param the specific {@code ApplicationEvent} subclass to listen to + */ +class GenericApplicationListenerDelegate implements GenericApplicationListener { + + private final Class supportedEventType; + + private final Consumer consumer; + + + GenericApplicationListenerDelegate(Class supportedEventType, Consumer consumer) { + this.supportedEventType = supportedEventType; + this.consumer = consumer; + } + + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.consumer.accept(this.supportedEventType.cast(event)); + } + + @Override + public boolean supportsEventType(ResolvableType eventType) { + return this.supportedEventType.isAssignableFrom(eventType.toClass()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index e670adb4191a..a9a067a92791 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.context.event; import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -83,6 +84,10 @@ public SimpleApplicationEventMulticaster(BeanFactory beanFactory) { * until all listeners have been executed. However, note that asynchronous execution * will not participate in the caller's thread context (class loader, transaction context) * unless the TaskExecutor explicitly supports this. + *

{@link ApplicationListener} instances which declare no support for asynchronous + * execution ({@link ApplicationListener#supportsAsyncExecution()} always run within + * the original thread which published the event, e.g. the transaction-synchronized + * {@link org.springframework.transaction.event.TransactionalApplicationListener}. * @since 2.0 * @see org.springframework.core.task.SyncTaskExecutor * @see org.springframework.core.task.SimpleAsyncTaskExecutor @@ -138,8 +143,14 @@ public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType even ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event)); Executor executor = getTaskExecutor(); for (ApplicationListener listener : getApplicationListeners(event, type)) { - if (executor != null) { - executor.execute(() -> invokeListener(listener, event)); + if (executor != null && listener.supportsAsyncExecution()) { + try { + executor.execute(() -> invokeListener(listener, event)); + } + catch (RejectedExecutionException ex) { + // Probably on shutdown -> invoke listener locally instead + invokeListener(listener, event); + } } else { invokeListener(listener, event); diff --git a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java index 11d29e30d7db..c98d60fa595e 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java +++ b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Arrays; +import org.springframework.core.KotlinDetector; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; @@ -37,6 +38,7 @@ * * @author Stephane Nicoll * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 4.2 */ public class MethodBasedEvaluationContext extends StandardEvaluationContext { @@ -55,7 +57,8 @@ public MethodBasedEvaluationContext(Object rootObject, Method method, Object[] a super(rootObject); this.method = method; - this.arguments = arguments; + this.arguments = (KotlinDetector.isSuspendingFunction(method) ? + Arrays.copyOf(arguments, arguments.length - 1) : arguments); this.parameterNameDiscoverer = parameterNameDiscoverer; } diff --git a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java index 29bb92402fdf..88077591ef47 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java +++ b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.beans.factory.BeanExpressionException; import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.core.SpringProperties; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.Expression; @@ -47,6 +48,7 @@ * beans such as "environment", "systemProperties" and "systemEnvironment". * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 * @see BeanExpressionContext#getBeanFactory() * @see org.springframework.expression.ExpressionParser @@ -55,6 +57,14 @@ */ public class StandardBeanExpressionResolver implements BeanExpressionResolver { + /** + * System property to configure the maximum length for SpEL expressions: {@value}. + *

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

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

As of Spring Framework 6.1.3, the maximum SpEL expression length can be + * configured via the {@link #MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME} property. * @param beanClassLoader the factory's bean class loader */ public StandardBeanExpressionResolver(@Nullable ClassLoader beanClassLoader) { - this.expressionParser = new SpelExpressionParser(new SpelParserConfiguration(null, beanClassLoader)); + SpelParserConfiguration parserConfig = new SpelParserConfiguration( + null, beanClassLoader, false, false, Integer.MAX_VALUE, retrieveMaxExpressionLength()); + this.expressionParser = new SpelExpressionParser(parserConfig); } @@ -178,4 +194,22 @@ public Object evaluate(@Nullable String value, BeanExpressionContext beanExpress protected void customizeEvaluationContext(StandardEvaluationContext evalContext) { } + private static int retrieveMaxExpressionLength() { + String value = SpringProperties.getProperty(MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME); + if (!StringUtils.hasText(value)) { + return SpelParserConfiguration.DEFAULT_MAX_EXPRESSION_LENGTH; + } + + try { + int maxLength = Integer.parseInt(value.trim()); + Assert.isTrue(maxLength > 0, () -> "Value [" + maxLength + "] for system property [" + + MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME + "] must be positive"); + return maxLength; + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Failed to parse value for system property [" + + MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME + "]: " + ex.getMessage(), ex); + } + } + } diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java index 5d9c0becb112..89d5db248a6e 100644 --- a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,9 @@ * * @author Stephane Nicoll * @since 5.0 + * @deprecated as of 6.1, in favor of the AOT engine. */ +@Deprecated(since = "6.1", forRemoval = true) public class CandidateComponentsIndex { private static final AntPathMatcher pathMatcher = new AntPathMatcher("."); diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java index ee95954b10ee..194c96df705b 100644 --- a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,10 @@ * * @author Stephane Nicoll * @since 5.0 + * @deprecated as of 6.1, in favor of the AOT engine. */ +@Deprecated(since = "6.1", forRemoval = true) +@SuppressWarnings("removal") public final class CandidateComponentsIndexLoader { /** diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 36b2c76f8387..775eab6898ed 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -34,6 +36,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.CachedIntrospectionResults; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; @@ -135,17 +138,6 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { - /** - * The name of the {@link LifecycleProcessor} bean in the context. - * If none is supplied, a {@link DefaultLifecycleProcessor} is used. - * @since 3.0 - * @see org.springframework.context.LifecycleProcessor - * @see org.springframework.context.support.DefaultLifecycleProcessor - * @see #start() - * @see #stop() - */ - public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; - /** * The name of the {@link MessageSource} bean in the context. * If none is supplied, message resolution is delegated to the parent. @@ -166,6 +158,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader */ public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + /** + * The name of the {@link LifecycleProcessor} bean in the context. + * If none is supplied, a {@link DefaultLifecycleProcessor} is used. + * @since 3.0 + * @see org.springframework.context.LifecycleProcessor + * @see org.springframework.context.support.DefaultLifecycleProcessor + * @see #start() + * @see #stop() + */ + public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; + static { // Eagerly load the ContextClosedEvent class to avoid weird classloader issues @@ -203,8 +206,12 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader /** Flag that indicates whether this context has been closed already. */ private final AtomicBoolean closed = new AtomicBoolean(); - /** Synchronization monitor for the "refresh" and "destroy". */ - private final Object startupShutdownMonitor = new Object(); + /** Synchronization lock for "refresh" and "close". */ + private final Lock startupShutdownLock = new ReentrantLock(); + + /** Currently active startup/shutdown thread. */ + @Nullable + private volatile Thread startupShutdownThread; /** Reference to the JVM shutdown hook, if registered. */ @Nullable @@ -441,8 +448,8 @@ protected void publishEvent(Object event, @Nullable ResolvableType typeHint) { if (this.earlyApplicationEvents != null) { this.earlyApplicationEvents.add(applicationEvent); } - else { - getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); + else if (this.applicationEventMulticaster != null) { + this.applicationEventMulticaster.multicastEvent(applicationEvent, eventType); } // Publish event via parent context as well... @@ -576,7 +583,10 @@ public Collection> getApplicationListeners() { @Override public void refresh() throws BeansException, IllegalStateException { - synchronized (this.startupShutdownMonitor) { + this.startupShutdownLock.lock(); + try { + this.startupShutdownThread = Thread.currentThread(); + StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh"); // Prepare this context for refreshing. @@ -595,7 +605,6 @@ public void refresh() throws BeansException, IllegalStateException { StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); - // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); beanPostProcess.end(); @@ -619,7 +628,7 @@ public void refresh() throws BeansException, IllegalStateException { finishRefresh(); } - catch (BeansException ex) { + catch (RuntimeException | Error ex ) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); @@ -636,12 +645,13 @@ public void refresh() throws BeansException, IllegalStateException { } finally { - // Reset common introspection caches in Spring's core, since we - // might not ever need metadata for singleton beans anymore... - resetCommonCaches(); contextRefresh.end(); } } + finally { + this.startupShutdownThread = null; + this.startupShutdownLock.unlock(); + } } /** @@ -797,8 +807,9 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa } /** - * Initialize the MessageSource. - * Use parent's if none defined in this context. + * Initialize the {@link MessageSource}. + *

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

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

Uses {@link DefaultLifecycleProcessor} if none defined in the context. + * @since 3.0 + * @see #LIFECYCLE_PROCESSOR_BEAN_NAME * @see org.springframework.context.support.DefaultLifecycleProcessor */ protected void initLifecycleProcessor() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { - this.lifecycleProcessor = - beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); + this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); if (logger.isTraceEnabled()) { logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]"); } @@ -937,7 +950,15 @@ protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory b // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); for (String weaverAwareName : weaverAwareNames) { - getBean(weaverAwareName); + try { + beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class); + } + catch (BeanNotOfRequiredTypeException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to initialize LoadTimeWeaverAware bean '" + weaverAwareName + + "' due to unexpected type mismatch: " + ex.getMessage()); + } + } } // Stop using the temporary ClassLoader for type matching. @@ -956,6 +977,9 @@ protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory b * {@link org.springframework.context.event.ContextRefreshedEvent}. */ protected void finishRefresh() { + // Reset common introspection caches in Spring's core infrastructure. + resetCommonCaches(); + // Clear context-level resource caches (such as ASM metadata from scanning). clearResourceCaches(); @@ -974,8 +998,11 @@ protected void finishRefresh() { * after an exception got thrown. * @param ex the exception that led to the cancellation */ - protected void cancelRefresh(BeansException ex) { + protected void cancelRefresh(Throwable ex) { this.active.set(false); + + // Reset common introspection caches in Spring's core infrastructure. + resetCommonCaches(); } /** @@ -1013,15 +1040,47 @@ public void registerShutdownHook() { this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) { @Override public void run() { - synchronized (startupShutdownMonitor) { + if (isStartupShutdownThreadStuck()) { + active.set(false); + return; + } + startupShutdownLock.lock(); + try { doClose(); } + finally { + startupShutdownLock.unlock(); + } } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } } + /** + * Determine whether an active startup/shutdown thread is currently stuck, + * e.g. through a {@code System.exit} call in a user component. + */ + private boolean isStartupShutdownThreadStuck() { + Thread activeThread = this.startupShutdownThread; + if (activeThread != null && activeThread.getState() == Thread.State.WAITING) { + // Indefinitely waiting: might be Thread.join or the like, or System.exit + activeThread.interrupt(); + try { + // Leave just a little bit of time for the interruption to show effect + Thread.sleep(1); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + if (activeThread.getState() == Thread.State.WAITING) { + // Interrupted but still waiting: very likely a System.exit call + return true; + } + } + return false; + } + /** * Close this application context, destroying all beans in its bean factory. *

Delegates to {@code doClose()} for the actual closing procedure. @@ -1031,8 +1090,17 @@ public void run() { */ @Override public void close() { - synchronized (this.startupShutdownMonitor) { + if (isStartupShutdownThreadStuck()) { + this.active.set(false); + return; + } + + this.startupShutdownLock.lock(); + try { + this.startupShutdownThread = Thread.currentThread(); + doClose(); + // If we registered a JVM shutdown hook, we don't need it anymore now: // We've already explicitly closed the context. if (this.shutdownHook != null) { @@ -1044,6 +1112,10 @@ public void close() { } } } + finally { + this.startupShutdownThread = null; + this.startupShutdownLock.unlock(); + } } /** @@ -1098,6 +1170,11 @@ protected void doClose() { this.applicationListeners.addAll(this.earlyApplicationListeners); } + // Reset internal delegates. + this.applicationEventMulticaster = null; + this.messageSource = null; + this.lifecycleProcessor = null; + // Switch to inactive. this.active.set(false); } @@ -1395,6 +1472,7 @@ protected BeanFactory getInternalParentBeanFactory() { //--------------------------------------------------------------------- @Override + @Nullable public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { return getMessageSource().getMessage(code, args, defaultMessage, locale); } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java index d58ceadb5132..2f3840ed486a 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java @@ -137,6 +137,7 @@ protected boolean isUseCodeAsDefaultMessage() { @Override + @Nullable public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { String msg = getMessageInternal(code, args, locale); if (msg != null) { diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java index 9c87844e9d71..5acf9148f2f8 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,6 +126,7 @@ protected final void refreshBeanFactory() throws BeansException { try { DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); + beanFactory.setApplicationStartup(getApplicationStartup()); customizeBeanFactory(beanFactory); loadBeanDefinitions(beanFactory); this.beanFactory = beanFactory; @@ -136,7 +137,7 @@ protected final void refreshBeanFactory() throws BeansException { } @Override - protected void cancelRefresh(BeansException ex) { + protected void cancelRefresh(Throwable ex) { DefaultListableBeanFactory beanFactory = this.beanFactory; if (beanFactory != null) { beanFactory.setSerializationId(null); diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java index 37ffa85e40fd..0d633af00716 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,6 +110,7 @@ public void addBasenames(String... basenames) { * in the order of registration. *

Calling code may introspect this set as well as add or remove entries. * @since 4.3 + * @see #setBasenames * @see #addBasenames */ public Set getBasenameSet() { diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java index df003f083fd5..1da55ce8fe50 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,12 +32,15 @@ import org.springframework.util.StringValueResolver; /** - * {@link BeanPostProcessor} implementation that supplies the {@code ApplicationContext}, - * {@link org.springframework.core.env.Environment Environment}, or - * {@link StringValueResolver} for the {@code ApplicationContext} to beans that - * implement the {@link EnvironmentAware}, {@link EmbeddedValueResolverAware}, - * {@link ResourceLoaderAware}, {@link ApplicationEventPublisherAware}, - * {@link MessageSourceAware}, and/or {@link ApplicationContextAware} interfaces. + * {@link BeanPostProcessor} implementation that supplies the + * {@link org.springframework.context.ApplicationContext ApplicationContext}, + * {@link org.springframework.core.env.Environment Environment}, + * {@link StringValueResolver}, or + * {@link org.springframework.core.metrics.ApplicationStartup ApplicationStartup} + * for the {@code ApplicationContext} to beans that implement the {@link EnvironmentAware}, + * {@link EmbeddedValueResolverAware}, {@link ResourceLoaderAware}, + * {@link ApplicationEventPublisherAware}, {@link MessageSourceAware}, + * {@link ApplicationStartupAware}, and/or {@link ApplicationContextAware} interfaces. * *

Implemented interfaces are satisfied in the order in which they are * mentioned above. @@ -55,6 +58,7 @@ * @see org.springframework.context.ResourceLoaderAware * @see org.springframework.context.ApplicationEventPublisherAware * @see org.springframework.context.MessageSourceAware + * @see org.springframework.context.ApplicationStartupAware * @see org.springframework.context.ApplicationContextAware * @see org.springframework.context.support.AbstractApplicationContext#refresh() */ @@ -77,40 +81,33 @@ public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicati @Override @Nullable public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware || - bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware || - bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware || - bean instanceof ApplicationStartupAware)) { - return bean; + if (bean instanceof Aware) { + invokeAwareInterfaces(bean); } - - invokeAwareInterfaces(bean); return bean; } private void invokeAwareInterfaces(Object bean) { - if (bean instanceof Aware) { - if (bean instanceof EnvironmentAware environmentAware) { - environmentAware.setEnvironment(this.applicationContext.getEnvironment()); - } - if (bean instanceof EmbeddedValueResolverAware embeddedValueResolverAware) { - embeddedValueResolverAware.setEmbeddedValueResolver(this.embeddedValueResolver); - } - if (bean instanceof ResourceLoaderAware resourceLoaderAware) { - resourceLoaderAware.setResourceLoader(this.applicationContext); - } - if (bean instanceof ApplicationEventPublisherAware applicationEventPublisherAware) { - applicationEventPublisherAware.setApplicationEventPublisher(this.applicationContext); - } - if (bean instanceof MessageSourceAware messageSourceAware) { - messageSourceAware.setMessageSource(this.applicationContext); - } - if (bean instanceof ApplicationStartupAware applicationStartupAware) { - applicationStartupAware.setApplicationStartup(this.applicationContext.getApplicationStartup()); - } - if (bean instanceof ApplicationContextAware applicationContextAware) { - applicationContextAware.setApplicationContext(this.applicationContext); - } + if (bean instanceof EnvironmentAware environmentAware) { + environmentAware.setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof EmbeddedValueResolverAware embeddedValueResolverAware) { + embeddedValueResolverAware.setEmbeddedValueResolver(this.embeddedValueResolver); + } + if (bean instanceof ResourceLoaderAware resourceLoaderAware) { + resourceLoaderAware.setResourceLoader(this.applicationContext); + } + if (bean instanceof ApplicationEventPublisherAware applicationEventPublisherAware) { + applicationEventPublisherAware.setApplicationEventPublisher(this.applicationContext); + } + if (bean instanceof MessageSourceAware messageSourceAware) { + messageSourceAware.setMessageSource(this.applicationContext); + } + if (bean instanceof ApplicationStartupAware applicationStartupAware) { + applicationStartupAware.setApplicationStartup(this.applicationContext.getApplicationStartup()); + } + if (bean instanceof ApplicationContextAware applicationContextAware) { + applicationContextAware.setApplicationContext(this.applicationContext); } } diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java index b2b5cec60415..342839892a8f 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.context.support; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; @@ -28,7 +29,6 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; /** * {@code BeanPostProcessor} that detects beans which implement the {@code ApplicationListener} @@ -114,14 +114,13 @@ public boolean requiresDestruction(Object bean) { @Override public boolean equals(@Nullable Object other) { - return (this == other || - (other instanceof ApplicationListenerDetector applicationListenerDectector && - this.applicationContext == applicationListenerDectector.applicationContext)); + return (this == other || (other instanceof ApplicationListenerDetector that && + this.applicationContext == that.applicationContext)); } @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.applicationContext); + return Objects.hashCode(this.applicationContext); } } diff --git a/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java index 998d061ca2cd..525d916ca0c5 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java +++ b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java @@ -123,6 +123,7 @@ protected boolean isEligibleForOverriding(String className) { } @Override + @Nullable protected Class loadClassForOverriding(String name) throws ClassNotFoundException { byte[] bytes = bytesCache.get(name); if (bytes == null) { diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 44dca4ad5ed8..bd351637b40a 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -26,11 +26,17 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.crac.CheckpointException; +import org.crac.Core; +import org.crac.RestoreException; +import org.crac.management.CRaCMXBean; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -41,18 +47,59 @@ import org.springframework.context.LifecycleProcessor; import org.springframework.context.Phased; import org.springframework.context.SmartLifecycle; +import org.springframework.core.NativeDetector; +import org.springframework.core.SpringProperties; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** - * Default implementation of the {@link LifecycleProcessor} strategy. + * Spring's default implementation of the {@link LifecycleProcessor} strategy. + * + *

Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in + * groups for specific phases, on startup/shutdown as well as for explicit start/stop + * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}. + * + *

As of 6.1, this also includes support for JVM checkpoint/restore (Project CRaC) + * when the {@code org.crac:crac} dependency on the classpath. * * @author Mark Fisher * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 3.0 */ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware { + /** + * Property name for a common context checkpoint: {@value}. + * @since 6.1 + * @see #ON_REFRESH_VALUE + * @see org.crac.Core#checkpointRestore() + */ + public static final String CHECKPOINT_PROPERTY_NAME = "spring.context.checkpoint"; + + /** + * Property name for terminating the JVM when the context reaches a specific phase: {@value}. + * @since 6.1 + * @see #ON_REFRESH_VALUE + */ + public static final String EXIT_PROPERTY_NAME = "spring.context.exit"; + + /** + * Recognized value for the context checkpoint and exit properties: {@value}. + * @since 6.1 + * @see #CHECKPOINT_PROPERTY_NAME + * @see #EXIT_PROPERTY_NAME + */ + public static final String ON_REFRESH_VALUE = "onRefresh"; + + + private static boolean checkpointOnRefresh = + ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(CHECKPOINT_PROPERTY_NAME)); + + private static final boolean exitOnRefresh = + ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(EXIT_PROPERTY_NAME)); + private final Log logger = LogFactory.getLog(getClass()); private volatile long timeoutPerShutdownPhase = 30000; @@ -62,6 +109,24 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor @Nullable private volatile ConfigurableListableBeanFactory beanFactory; + @Nullable + private volatile Set stoppedBeans; + + // Just for keeping a strong reference to the registered CRaC Resource, if any + @Nullable + private Object cracResource; + + + public DefaultLifecycleProcessor() { + if (!NativeDetector.inNativeImage() && ClassUtils.isPresent("org.crac.Core", getClass().getClassLoader())) { + this.cracResource = new CracDelegate().registerResource(); + } + else if (checkpointOnRefresh) { + throw new IllegalStateException( + "Checkpoint on refresh requires a CRaC-enabled JVM and 'org.crac:crac' on the classpath"); + } + } + /** * Specify the maximum time allotted in milliseconds for the shutdown of any @@ -101,7 +166,10 @@ private ConfigurableListableBeanFactory getBeanFactory() { */ @Override public void start() { + this.stoppedBeans = null; startBeans(false); + // If any bean failed to explicitly start, the exception propagates here. + // The caller may choose to subsequently call stop() if appropriate. this.running = true; } @@ -121,7 +189,24 @@ public void stop() { @Override public void onRefresh() { - startBeans(true); + if (checkpointOnRefresh) { + checkpointOnRefresh = false; + new CracDelegate().checkpointRestore(); + } + if (exitOnRefresh) { + Runtime.getRuntime().halt(0); + } + + this.stoppedBeans = null; + try { + startBeans(true); + } + catch (ApplicationContextException ex) { + // Some bean failed to auto-start within context refresh: + // stop already started beans on context refresh failure. + stopBeans(); + throw ex; + } this.running = true; } @@ -139,24 +224,46 @@ public boolean isRunning() { // Internal helpers + void stopForRestart() { + if (this.running) { + this.stoppedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>()); + stopBeans(); + this.running = false; + } + } + + void restartAfterStop() { + if (this.stoppedBeans != null) { + startBeans(true); + this.stoppedBeans = null; + this.running = true; + } + } + private void startBeans(boolean autoStartupOnly) { Map lifecycleBeans = getLifecycleBeans(); Map phases = new TreeMap<>(); lifecycleBeans.forEach((beanName, bean) -> { - if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())) { - int phase = getPhase(bean); - phases.computeIfAbsent( - phase, - p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) + if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { + int startupPhase = getPhase(bean); + phases.computeIfAbsent(startupPhase, + phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) ).add(beanName, bean); } }); + if (!phases.isEmpty()) { phases.values().forEach(LifecycleGroup::start); } } + private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) { + Set stoppedBeans = this.stoppedBeans; + return (stoppedBeans != null ? stoppedBeans.contains(beanName) : + (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())); + } + /** * Start the specified bean as part of the given set of Lifecycle beans, * making sure that any beans that it depends on are started first. @@ -170,8 +277,7 @@ private void doStart(Map lifecycleBeans, String bea for (String dependency : dependenciesForBean) { doStart(lifecycleBeans, dependency, autoStartupOnly); } - if (!bean.isRunning() && - (!autoStartupOnly || !(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())) { + if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) { if (logger.isTraceEnabled()) { logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); } @@ -188,24 +294,25 @@ private void doStart(Map lifecycleBeans, String bea } } + private boolean toBeStarted(String beanName, Lifecycle bean) { + Set stoppedBeans = this.stoppedBeans; + return (stoppedBeans != null ? stoppedBeans.contains(beanName) : + (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())); + } + private void stopBeans() { Map lifecycleBeans = getLifecycleBeans(); - Map phases = new HashMap<>(); + Map phases = new TreeMap<>(Comparator.reverseOrder()); + lifecycleBeans.forEach((beanName, bean) -> { int shutdownPhase = getPhase(bean); - LifecycleGroup group = phases.get(shutdownPhase); - if (group == null) { - group = new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false); - phases.put(shutdownPhase, group); - } - group.add(beanName, bean); + phases.computeIfAbsent(shutdownPhase, + phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, false) + ).add(beanName, bean); }); + if (!phases.isEmpty()) { - List keys = new ArrayList<>(phases.keySet()); - keys.sort(Collections.reverseOrder()); - for (Integer key : keys) { - phases.get(key).stop(); - } + phases.values().forEach(LifecycleGroup::stop); } } @@ -226,6 +333,10 @@ private void doStop(Map lifecycleBeans, final Strin } try { if (bean.isRunning()) { + Set stoppedBeans = this.stoppedBeans; + if (stoppedBeans != null) { + stoppedBeans.add(beanName); + } if (bean instanceof SmartLifecycle smartLifecycle) { if (logger.isTraceEnabled()) { logger.trace("Asking bean '" + beanName + "' of type [" + @@ -260,6 +371,9 @@ else if (bean instanceof SmartLifecycle) { if (logger.isWarnEnabled()) { logger.warn("Failed to stop bean '" + beanName + "'", ex); } + if (bean instanceof SmartLifecycle) { + latch.countDown(); + } } } } @@ -314,6 +428,8 @@ protected int getPhase(Lifecycle bean) { /** * Helper class for maintaining a group of Lifecycle beans that should be started * and stopped together based on their 'phase' value (or the default value of 0). + * The group is expected to be created in an ad-hoc fashion and group members are + * expected to always have the same 'phase' value. */ private class LifecycleGroup { @@ -352,7 +468,6 @@ public void start() { if (logger.isDebugEnabled()) { logger.debug("Starting beans in phase " + this.phase); } - Collections.sort(this.members); for (LifecycleGroupMember member : this.members) { doStart(this.lifecycleBeans, member.name, this.autoStartupOnly); } @@ -365,7 +480,6 @@ public void stop() { if (logger.isDebugEnabled()) { logger.debug("Stopping beans in phase " + this.phase); } - this.members.sort(Collections.reverseOrder()); CountDownLatch latch = new CountDownLatch(this.smartMemberCount); Set countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet<>()); Set lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet()); @@ -381,9 +495,9 @@ else if (member.bean instanceof SmartLifecycle) { try { latch.await(this.timeout, TimeUnit.MILLISECONDS); if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) { - logger.info("Failed to shut down " + countDownBeanNames.size() + " bean" + - (countDownBeanNames.size() > 1 ? "s" : "") + " with phase value " + - this.phase + " within timeout of " + this.timeout + "ms: " + countDownBeanNames); + logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() + + " bean" + (countDownBeanNames.size() > 1 ? "s" : "") + + " still running after timeout of " + this.timeout + "ms: " + countDownBeanNames); } } catch (InterruptedException ex) { @@ -394,24 +508,97 @@ else if (member.bean instanceof SmartLifecycle) { /** - * Adapts the Comparable interface onto the lifecycle phase model. + * A simple record of a LifecycleGroup member. */ - private class LifecycleGroupMember implements Comparable { + private record LifecycleGroupMember(String name, Lifecycle bean) {} + + + /** + * Inner class to avoid a hard dependency on Project CRaC at runtime. + * @since 6.1 + * @see org.crac.Core + */ + private class CracDelegate { + + public Object registerResource() { + logger.debug("Registering JVM checkpoint/restore callback for Spring-managed lifecycle beans"); + CracResourceAdapter resourceAdapter = new CracResourceAdapter(); + org.crac.Core.getGlobalContext().register(resourceAdapter); + return resourceAdapter; + } + + public void checkpointRestore() { + logger.info("Triggering JVM checkpoint/restore"); + try { + Core.checkpointRestore(); + } + catch (UnsupportedOperationException ex) { + throw new ApplicationContextException("CRaC checkpoint not supported on current JVM", ex); + } + catch (CheckpointException ex) { + throw new ApplicationContextException("Failed to take CRaC checkpoint on refresh", ex); + } + catch (RestoreException ex) { + throw new ApplicationContextException("Failed to restore CRaC checkpoint on refresh", ex); + } + } + } - private final String name; - private final Lifecycle bean; + /** + * Resource adapter for Project CRaC, triggering a stop-and-restart cycle + * for Spring-managed lifecycle beans around a JVM checkpoint/restore. + * @since 6.1 + * @see #stopForRestart() + * @see #restartAfterStop() + */ + private class CracResourceAdapter implements org.crac.Resource { - LifecycleGroupMember(String name, Lifecycle bean) { - this.name = name; - this.bean = bean; + @Nullable + private CyclicBarrier barrier; + + @Override + public void beforeCheckpoint(org.crac.Context context) { + // A non-daemon thread for preventing an accidental JVM shutdown before the checkpoint + this.barrier = new CyclicBarrier(2); + + Thread thread = new Thread(() -> { + awaitPreventShutdownBarrier(); + // Checkpoint happens here + awaitPreventShutdownBarrier(); + }, "prevent-shutdown"); + + thread.setDaemon(false); + thread.start(); + awaitPreventShutdownBarrier(); + + logger.debug("Stopping Spring-managed lifecycle beans before JVM checkpoint"); + stopForRestart(); } @Override - public int compareTo(LifecycleGroupMember other) { - int thisPhase = getPhase(this.bean); - int otherPhase = getPhase(other.bean); - return Integer.compare(thisPhase, otherPhase); + public void afterRestore(org.crac.Context context) { + logger.info("Restarting Spring-managed lifecycle beans after JVM restore"); + restartAfterStop(); + + // Barrier for prevent-shutdown thread not needed anymore + this.barrier = null; + + if (!checkpointOnRefresh) { + logger.info("Spring-managed lifecycle restart completed (restored JVM running for " + + CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore() + " ms)"); + } + } + + private void awaitPreventShutdownBarrier() { + try { + if (this.barrier != null) { + this.barrier.await(); + } + } + catch (Exception ex) { + logger.trace("Exception from prevent-shutdown barrier", ex); + } } } diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java index 281b8e7986d4..df09bc8957f9 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java @@ -179,10 +179,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(getCodes()); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArguments()); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getDefaultMessage()); - return hashCode; + return ObjectUtils.nullSafeHash(getCode(), getArguments(), getDefaultMessage()); } } diff --git a/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java index f592996fee31..afe4db3e89b0 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,7 +95,7 @@ public String getMessage(MessageSourceResolvable resolvable, Locale locale) thro @Override public String toString() { - return this.parentMessageSource != null ? this.parentMessageSource.toString() : "Empty MessageSource"; + return (this.parentMessageSource != null ? this.parentMessageSource.toString() : "Empty MessageSource"); } } diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java index 85da53b39b0f..ab6a509b4b00 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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.IOException; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -297,7 +298,7 @@ protected final void refreshBeanFactory() throws IllegalStateException { } @Override - protected void cancelRefresh(BeansException ex) { + protected void cancelRefresh(Throwable ex) { this.beanFactory.setSerializationId(null); super.cancelRefresh(ex); } @@ -360,6 +361,11 @@ public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefini return this.beanFactory.getBeanDefinition(beanName); } + @Override + public boolean isBeanDefinitionOverridable(String beanName) { + return this.beanFactory.isBeanDefinitionOverridable(beanName); + } + @Override public boolean isBeanNameInUse(String beanName) { return this.beanFactory.isBeanNameInUse(beanName); @@ -423,16 +429,37 @@ private void preDetermineBeanTypes(RuntimeHints runtimeHints) { PostProcessorRegistrationDelegate.loadBeanPostProcessors( this.beanFactory, SmartInstantiationAwareBeanPostProcessor.class); + List lazyBeans = new ArrayList<>(); + + // First round: non-lazy singleton beans in definition order, + // matching preInstantiateSingletons. for (String beanName : this.beanFactory.getBeanDefinitionNames()) { - Class beanType = this.beanFactory.getType(beanName); - if (beanType != null) { - ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints); - for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) { - Class newBeanType = bpp.determineBeanType(beanType, beanName); - if (newBeanType != beanType) { - ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints); - beanType = newBeanType; - } + BeanDefinition bd = getBeanDefinition(beanName); + if (bd.isSingleton() && !bd.isLazyInit()) { + preDetermineBeanType(beanName, bpps, runtimeHints); + } + else { + lazyBeans.add(beanName); + } + } + + // Second round: lazy singleton beans and scoped beans. + for (String beanName : lazyBeans) { + preDetermineBeanType(beanName, bpps, runtimeHints); + } + } + + private void preDetermineBeanType(String beanName, List bpps, + RuntimeHints runtimeHints) { + + Class beanType = this.beanFactory.getType(beanName); + if (beanType != null) { + ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints); + for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) { + Class newBeanType = bpp.determineBeanType(beanType, beanName); + if (newBeanType != beanType) { + ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints); + beanType = newBeanType; } } } @@ -575,6 +602,10 @@ public ClassDerivedBeanDefinition(ClassDerivedBeanDefinition original) { @Override @Nullable public Constructor[] getPreferredConstructors() { + Constructor[] fromAttribute = super.getPreferredConstructors(); + if (fromAttribute != null) { + return fromAttribute; + } Class clazz = getBeanClass(); Constructor primaryCtor = BeanUtils.findPrimaryConstructor(clazz); if (primaryCtor != null) { diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java index 79acbfaf2d94..9aaaf33bcdfc 100644 --- a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ package org.springframework.context.support; import java.text.MessageFormat; -import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -53,7 +53,7 @@ public abstract class MessageSourceSupport { * Used for passed-in default messages. MessageFormats for resolved * codes are cached on a specific basis in subclasses. */ - private final Map> messageFormatsPerMessage = new HashMap<>(); + private final Map> messageFormatsPerMessage = new ConcurrentHashMap<>(); /** @@ -116,32 +116,22 @@ protected String formatMessage(String msg, @Nullable Object[] args, Locale local if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { return msg; } - MessageFormat messageFormat = null; - synchronized (this.messageFormatsPerMessage) { - Map messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg); - if (messageFormatsPerLocale != null) { - messageFormat = messageFormatsPerLocale.get(locale); + Map messageFormatsPerLocale = this.messageFormatsPerMessage + .computeIfAbsent(msg, key -> new ConcurrentHashMap<>()); + MessageFormat messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> { + try { + return createMessageFormat(msg, locale); } - else { - messageFormatsPerLocale = new HashMap<>(); - this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale); - } - if (messageFormat == null) { - try { - messageFormat = createMessageFormat(msg, locale); - } - catch (IllegalArgumentException ex) { - // Invalid message format - probably not intended for formatting, - // rather using a message structure with no arguments involved... - if (isAlwaysUseMessageFormat()) { - throw ex; - } - // Silently proceed with raw message if format not enforced... - messageFormat = INVALID_MESSAGE_FORMAT; + catch (IllegalArgumentException ex) { + // Invalid message format - probably not intended for formatting, + // rather using a message structure with no arguments involved... + if (isAlwaysUseMessageFormat()) { + throw ex; } - messageFormatsPerLocale.put(locale, messageFormat); + // Silently proceed with raw message if format not enforced... + return INVALID_MESSAGE_FORMAT; } - } + }); if (messageFormat == INVALID_MESSAGE_FORMAT) { return msg; } diff --git a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java index d5b788487682..7735045a9808 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java +++ b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.BiConsumer; @@ -29,10 +30,12 @@ import org.springframework.beans.PropertyValue; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AbstractBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -227,7 +230,8 @@ public static void registerBeanPostProcessors( // a bean is created during BeanPostProcessor instantiation, i.e. when // a bean is not eligible for getting processed by all BeanPostProcessors. int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; - beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); + beanFactory.addBeanPostProcessor( + new BeanPostProcessorChecker(beanFactory, postProcessorNames, beanProcessorTargetCount)); // Separate between BeanPostProcessors that implement PriorityOrdered, // Ordered, and the rest. @@ -309,8 +313,9 @@ static List loadBeanPostProcessors( /** * Selectively invoke {@link MergedBeanDefinitionPostProcessor} instances - * registered in the specified bean factory, resolving bean definitions as - * well as any inner bean definitions that they may contain. + * registered in the specified bean factory, resolving bean definitions and + * any attributes if necessary as well as any inner bean definitions that + * they may contain. * @param beanFactory the bean factory to use */ static void invokeMergedBeanDefinitionPostProcessors(DefaultListableBeanFactory beanFactory) { @@ -389,10 +394,15 @@ private static final class BeanPostProcessorChecker implements BeanPostProcessor private final ConfigurableListableBeanFactory beanFactory; + private final String[] postProcessorNames; + private final int beanPostProcessorTargetCount; - public BeanPostProcessorChecker(ConfigurableListableBeanFactory beanFactory, int beanPostProcessorTargetCount) { + public BeanPostProcessorChecker(ConfigurableListableBeanFactory beanFactory, + String[] postProcessorNames, int beanPostProcessorTargetCount) { + this.beanFactory = beanFactory; + this.postProcessorNames = postProcessorNames; this.beanPostProcessorTargetCount = beanPostProcessorTargetCount; } @@ -405,10 +415,30 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) { public Object postProcessAfterInitialization(Object bean, String beanName) { if (!(bean instanceof BeanPostProcessor) && !isInfrastructureBean(beanName) && this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount) { - if (logger.isInfoEnabled()) { - logger.info("Bean '" + beanName + "' of type [" + bean.getClass().getName() + + if (logger.isWarnEnabled()) { + Set bppsInCreation = new LinkedHashSet<>(2); + for (String bppName : this.postProcessorNames) { + if (this.beanFactory.isCurrentlyInCreation(bppName)) { + bppsInCreation.add(bppName); + } + } + if (bppsInCreation.size() == 1) { + String bppName = bppsInCreation.iterator().next(); + if (this.beanFactory.containsBeanDefinition(bppName) && + beanName.equals(this.beanFactory.getBeanDefinition(bppName).getFactoryBeanName())) { + logger.warn("Bean '" + beanName + "' of type [" + bean.getClass().getName() + + "] is not eligible for getting processed by all BeanPostProcessors " + + "(for example: not eligible for auto-proxying). The currently created " + + "BeanPostProcessor " + bppsInCreation + " is declared through a non-static " + + "factory method on that class; consider declaring it as static instead."); + return bean; + } + } + logger.warn("Bean '" + beanName + "' of type [" + bean.getClass().getName() + "] is not eligible for getting processed by all BeanPostProcessors " + - "(for example: not eligible for auto-proxying)"); + "(for example: not eligible for auto-proxying). Is this bean getting eagerly " + + "injected into a currently created BeanPostProcessor " + bppsInCreation + "? " + + "Check the corresponding BeanPostProcessor declaration and its dependencies."); } } return bean; @@ -450,20 +480,32 @@ private void postProcessRootBeanDefinition(List postProcessor.postProcessMergedBeanDefinition(bd, beanType, beanName)); for (PropertyValue propertyValue : bd.getPropertyValues().getPropertyValueList()) { - Object value = propertyValue.getValue(); - if (value instanceof AbstractBeanDefinition innerBd) { - Class innerBeanType = resolveBeanType(innerBd); - resolveInnerBeanDefinition(valueResolver, innerBd, (innerBeanName, innerBeanDefinition) - -> postProcessRootBeanDefinition(postProcessors, innerBeanName, innerBeanType, innerBeanDefinition)); - } + postProcessValue(postProcessors, valueResolver, propertyValue.getValue()); } for (ValueHolder valueHolder : bd.getConstructorArgumentValues().getIndexedArgumentValues().values()) { - Object value = valueHolder.getValue(); - if (value instanceof AbstractBeanDefinition innerBd) { - Class innerBeanType = resolveBeanType(innerBd); - resolveInnerBeanDefinition(valueResolver, innerBd, (innerBeanName, innerBeanDefinition) - -> postProcessRootBeanDefinition(postProcessors, innerBeanName, innerBeanType, innerBeanDefinition)); - } + postProcessValue(postProcessors, valueResolver, valueHolder.getValue()); + } + for (ValueHolder valueHolder : bd.getConstructorArgumentValues().getGenericArgumentValues()) { + postProcessValue(postProcessors, valueResolver, valueHolder.getValue()); + } + } + + private void postProcessValue(List postProcessors, + BeanDefinitionValueResolver valueResolver, @Nullable Object value) { + if (value instanceof BeanDefinitionHolder bdh + && bdh.getBeanDefinition() instanceof AbstractBeanDefinition innerBd) { + + Class innerBeanType = resolveBeanType(innerBd); + resolveInnerBeanDefinition(valueResolver, innerBd, (innerBeanName, innerBeanDefinition) + -> postProcessRootBeanDefinition(postProcessors, innerBeanName, innerBeanType, innerBeanDefinition)); + } + else if (value instanceof AbstractBeanDefinition innerBd) { + Class innerBeanType = resolveBeanType(innerBd); + resolveInnerBeanDefinition(valueResolver, innerBd, (innerBeanName, innerBeanDefinition) + -> postProcessRootBeanDefinition(postProcessors, innerBeanName, innerBeanType, innerBeanDefinition)); + } + else if (value instanceof TypedStringValue typedStringValue) { + resolveTypeStringValue(typedStringValue); } } @@ -476,6 +518,15 @@ private void resolveInnerBeanDefinition(BeanDefinitionValueResolver valueResolve }); } + private void resolveTypeStringValue(TypedStringValue typedStringValue) { + try { + typedStringValue.resolveTargetType(this.beanFactory.getBeanClassLoader()); + } + catch (ClassNotFoundException ex) { + // ignore + } + } + private Class resolveBeanType(AbstractBeanDefinition bd) { if (!bd.hasBeanClass()) { try { diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index 6731999c0b83..a254cbd9fa76 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,14 @@ import java.io.InputStreamReader; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.springframework.context.ResourceLoaderAware; @@ -34,6 +36,8 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; import org.springframework.util.StringUtils; @@ -74,6 +78,7 @@ * this message source! * * @author Juergen Hoeller + * @author Sebastien Deleuze * @see #setCacheSeconds * @see #setBasenames * @see #setDefaultEncoding @@ -87,10 +92,10 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements ResourceLoaderAware { - private static final String PROPERTIES_SUFFIX = ".properties"; + private static final String XML_EXTENSION = ".xml"; - private static final String XML_SUFFIX = ".xml"; + private List fileExtensions = List.of(".properties", XML_EXTENSION); @Nullable private Properties fileEncodings; @@ -111,6 +116,22 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased private final ConcurrentMap cachedMergedProperties = new ConcurrentHashMap<>(); + /** + * Set the list of supported file extensions. + *

The default is a list containing {@code .properties} and {@code .xml}. + * @param fileExtensions the file extensions (starts with a dot) + * @since 6.1 + */ + public void setFileExtensions(List fileExtensions) { + Assert.isTrue(!CollectionUtils.isEmpty(fileExtensions), "At least one file extension is required"); + for (String extension : fileExtensions) { + if (!extension.startsWith(".")) { + throw new IllegalArgumentException("File extension '" + extension + "' should start with '.'"); + } + } + this.fileExtensions = Collections.unmodifiableList(fileExtensions); + } + /** * Set per-file charsets to use for parsing properties files. *

Only applies to classic properties files, not to XML files. @@ -170,6 +191,7 @@ public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { * returning the value found in the bundle as-is (without MessageFormat parsing). */ @Override + @Nullable protected String resolveCodeWithoutArguments(String code, Locale locale) { if (getCacheMillis() < 0) { PropertiesHolder propHolder = getMergedProperties(locale); @@ -230,36 +252,66 @@ protected MessageFormat resolveCode(String code, Locale locale) { *

Only used when caching resource bundle contents forever, i.e. * with cacheSeconds < 0. Therefore, merged properties are always * cached forever. + * @see #collectPropertiesToMerge + * @see #mergeProperties */ protected PropertiesHolder getMergedProperties(Locale locale) { PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale); if (mergedHolder != null) { return mergedHolder; } + mergedHolder = mergeProperties(collectPropertiesToMerge(locale)); + PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder); + if (existing != null) { + mergedHolder = existing; + } + return mergedHolder; + } - Properties mergedProps = newProperties(); - long latestTimestamp = -1; + /** + * Determine the properties to merge based on the specified basenames. + * @param locale the locale + * @return the list of properties holders + * @since 6.1.4 + * @see #getBasenameSet() + * @see #calculateAllFilenames + * @see #mergeProperties + */ + protected List collectPropertiesToMerge(Locale locale) { String[] basenames = StringUtils.toStringArray(getBasenameSet()); + List holders = new ArrayList<>(basenames.length); for (int i = basenames.length - 1; i >= 0; i--) { List filenames = calculateAllFilenames(basenames[i], locale); for (int j = filenames.size() - 1; j >= 0; j--) { String filename = filenames.get(j); PropertiesHolder propHolder = getProperties(filename); if (propHolder.getProperties() != null) { - mergedProps.putAll(propHolder.getProperties()); - if (propHolder.getFileTimestamp() > latestTimestamp) { - latestTimestamp = propHolder.getFileTimestamp(); - } + holders.add(propHolder); } } } + return holders; + } - mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp); - PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder); - if (existing != null) { - mergedHolder = existing; + /** + * Merge the given properties holders into a single holder. + * @param holders the list of properties holders + * @return a single merged properties holder + * @since 6.1.4 + * @see #newProperties() + * @see #getMergedProperties + * @see #collectPropertiesToMerge + */ + protected PropertiesHolder mergeProperties(List holders) { + Properties mergedProps = newProperties(); + long latestTimestamp = -1; + for (PropertiesHolder holder : holders) { + mergedProps.putAll(holder.getProperties()); + if (holder.getFileTimestamp() > latestTimestamp) { + latestTimestamp = holder.getFileTimestamp(); + } } - return mergedHolder; + return new PropertiesHolder(mergedProps, latestTimestamp); } /** @@ -400,20 +452,17 @@ protected PropertiesHolder getProperties(String filename) { /** * Refresh the PropertiesHolder for the given bundle filename. - * The holder can be {@code null} if not cached before, or a timed-out cache entry + *

The holder can be {@code null} if not cached before, or a timed-out cache entry * (potentially getting re-validated against the current last-modified timestamp). * @param filename the bundle filename (basename + Locale) * @param propHolder the current PropertiesHolder for the bundle + * @see #resolveResource(String) */ protected PropertiesHolder refreshProperties(String filename, @Nullable PropertiesHolder propHolder) { long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis()); - Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX); - if (!resource.exists()) { - resource = this.resourceLoader.getResource(filename + XML_SUFFIX); - } - - if (resource.exists()) { + Resource resource = resolveResource(filename); + if (resource != null) { long fileTimestamp = -1; if (getCacheMillis() >= 0) { // Last-modified timestamp of file will just be read if caching with timeout. @@ -451,7 +500,7 @@ protected PropertiesHolder refreshProperties(String filename, @Nullable Properti else { // Resource does not exist. if (logger.isDebugEnabled()) { - logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML"); + logger.debug("No properties file found for [" + filename + "]"); } // Empty holder representing "not found". propHolder = new PropertiesHolder(); @@ -462,6 +511,48 @@ protected PropertiesHolder refreshProperties(String filename, @Nullable Properti return propHolder; } + /** + * Resolve the specified bundle {@code filename} into a concrete {@link Resource}, + * potentially checking multiple sources or file extensions. + *

If no suitable concrete {@code Resource} can be resolved, this method + * returns a {@code Resource} for which {@link Resource#exists()} returns + * {@code false}, which gets subsequently ignored. + *

This can be leveraged to check the last modification timestamp or to load + * properties from alternative sources — for example, from an XML BLOB + * in a database, or from properties serialized using a custom format such as + * JSON. + *

The default implementation delegates to the configured + * {@link #setResourceLoader(ResourceLoader) ResourceLoader} to resolve + * resources, checking in order for existing {@code Resource} with extensions defined + * by {@link #setFileExtensions(List)} ({@code .properties} and {@code .xml} + * by default). + *

When overriding this method, {@link #loadProperties(Resource, String)} + * must be capable of loading properties from any type of + * {@code Resource} returned by this method. As a consequence, implementors + * are strongly encouraged to also override {@code loadProperties()}. + *

As an alternative to overriding this method, you can configure a + * {@link #setPropertiesPersister(PropertiesPersister) PropertiesPersister} + * that is capable of dealing with all resources returned by this method. + * Please note, however, that the default {@code loadProperties()} implementation + * uses {@link PropertiesPersister#loadFromXml(Properties, InputStream) loadFromXml} + * for XML resources and otherwise uses the two + * {@link PropertiesPersister#load(Properties, InputStream) load} methods + * for other types of resources. + * @param filename the bundle filename (basename + Locale) + * @return the {@code Resource} to use, or {@code null} if none found + * @since 6.1 + */ + @Nullable + protected Resource resolveResource(String filename) { + for (String fileExtension : this.fileExtensions) { + Resource resource = this.resourceLoader.getResource(filename + fileExtension); + if (resource.exists()) { + return resource; + } + } + return null; + } + /** * Load the properties from the given resource. * @param resource the resource to load from @@ -473,7 +564,7 @@ protected Properties loadProperties(Resource resource, String filename) throws I Properties props = newProperties(); try (InputStream is = resource.getInputStream()) { String resourceFilename = resource.getFilename(); - if (resourceFilename != null && resourceFilename.endsWith(XML_SUFFIX)) { + if (resourceFilename != null && resourceFilename.endsWith(XML_EXTENSION)) { if (logger.isDebugEnabled()) { logger.debug("Loading properties [" + resource.getFilename() + "]"); } @@ -561,7 +652,7 @@ protected class PropertiesHolder { private volatile long refreshTimestamp = -2; - private final ReentrantLock refreshLock = new ReentrantLock(); + private final Lock refreshLock = new ReentrantLock(); /** Cache to hold already generated MessageFormats per message code. */ private final ConcurrentMap> cachedMessageFormats = diff --git a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java index 012237889a49..026977b4fe2c 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java @@ -145,6 +145,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { * returning the value found in the bundle as-is (without MessageFormat parsing). */ @Override + @Nullable protected String resolveCodeWithoutArguments(String code, Locale locale) { Set basenames = getBasenameSet(); for (String basename : basenames) { diff --git a/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java index 0b237eb0b1c2..00af013192ef 100644 --- a/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java +++ b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,14 +55,8 @@ public class SimpleThreadScope implements Scope { private static final Log logger = LogFactory.getLog(SimpleThreadScope.class); - private final ThreadLocal> threadScope = - new NamedThreadLocal<>("SimpleThreadScope") { - @Override - protected Map initialValue() { - return new HashMap<>(); - } - }; - + private final ThreadLocal> threadScope = NamedThreadLocal.withInitial( + "SimpleThreadScope", HashMap::new); @Override public Object get(String name, ObjectFactory objectFactory) { diff --git a/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java index 035fad6581f5..14f6f88b10a1 100644 --- a/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,13 @@ import org.w3c.dom.Element; +import org.springframework.beans.BeanUtils; import org.springframework.jndi.JndiObjectFactoryBean; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} * implementation for parsing '{@code local-slsb}' tags and - * creating plain {@link JndiObjectFactoryBean} definitions. + * creating plain {@link JndiObjectFactoryBean} definitions on 6.0. * * @author Rob Harrop * @author Juergen Hoeller @@ -36,4 +37,10 @@ protected Class getBeanClass(Element element) { return JndiObjectFactoryBean.class; } + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null); + } + } diff --git a/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java index a88c5f646189..9768e0dfc337 100644 --- a/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,13 @@ import org.w3c.dom.Element; +import org.springframework.beans.BeanUtils; import org.springframework.jndi.JndiObjectFactoryBean; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} * implementation for parsing '{@code remote-slsb}' tags and - * creating plain {@link JndiObjectFactoryBean} definitions. + * creating plain {@link JndiObjectFactoryBean} definitions as of 6.0. * * @author Rob Harrop * @author Juergen Hoeller @@ -36,4 +37,10 @@ protected Class getBeanClass(Element element) { return JndiObjectFactoryBean.class; } + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null); + } + } diff --git a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java index c51f70b7ee48..2a8e5e4c8387 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java @@ -74,7 +74,7 @@ enum Style { /** * The default format for the annotated type: typically 'number' but possibly - * 'currency' for a money type (e.g. {@code javax.money.MonetaryAmount)}. + * 'currency' for a money type (e.g. {@code javax.money.MonetaryAmount}). * @since 4.2 */ DEFAULT, diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java index 06a95b7bee5a..0d6bde41743c 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,10 @@ import java.util.Collections; import java.util.Date; import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.TimeZone; import org.springframework.format.Formatter; @@ -35,9 +37,14 @@ /** * A formatter for {@link java.util.Date} types. + * *

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

Common ISO patterns for UTC instants are applied at millisecond precision. + * Note that {@link org.springframework.format.datetime.standard.InstantFormatter} + * is recommended for flexible UTC parsing into a {@link java.time.Instant} instead. + * * @author Keith Donald * @author Juergen Hoeller * @author Phillip Webb @@ -49,15 +56,23 @@ public class DateFormatter implements Formatter { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - // We use an EnumMap instead of Map.of(...) since the former provides better performance. private static final Map ISO_PATTERNS; + private static final Map ISO_FALLBACK_PATTERNS; + static { + // We use an EnumMap instead of Map.of(...) since the former provides better performance. Map formats = new EnumMap<>(ISO.class); formats.put(ISO.DATE, "yyyy-MM-dd"); formats.put(ISO.TIME, "HH:mm:ss.SSSXXX"); formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); ISO_PATTERNS = Collections.unmodifiableMap(formats); + + // Fallback format for the time part without milliseconds. + Map fallbackFormats = new EnumMap<>(ISO.class); + fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX"); + fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX"); + ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats); } @@ -202,8 +217,16 @@ public Date parse(String text, Locale locale) throws ParseException { return getDateFormat(locale).parse(text); } catch (ParseException ex) { + Set fallbackPatterns = new LinkedHashSet<>(); + String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso); + if (isoPattern != null) { + fallbackPatterns.add(isoPattern); + } if (!ObjectUtils.isEmpty(this.fallbackPatterns)) { - for (String pattern : this.fallbackPatterns) { + Collections.addAll(fallbackPatterns, this.fallbackPatterns); + } + if (!fallbackPatterns.isEmpty()) { + for (String pattern : fallbackPatterns) { try { DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale)); // Align timezone for parsing format with printing format if ISO is set. @@ -221,8 +244,8 @@ public Date parse(String text, Locale locale) throws ParseException { } if (this.source != null) { ParseException parseException = new ParseException( - String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), - ex.getErrorOffset()); + String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), + ex.getErrorOffset()); parseException.initCause(ex); throw parseException; } @@ -269,7 +292,7 @@ private DateFormat createDateFormat(Locale locale) { if (timeStyle != -1) { return DateFormat.getTimeInstance(timeStyle, locale); } - throw new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); + throw unsupportedStylePatternException(); } return DateFormat.getDateInstance(this.style, locale); @@ -277,15 +300,21 @@ private DateFormat createDateFormat(Locale locale) { private int getStylePatternForChar(int index) { if (this.stylePattern != null && this.stylePattern.length() > index) { - switch (this.stylePattern.charAt(index)) { - case 'S': return DateFormat.SHORT; - case 'M': return DateFormat.MEDIUM; - case 'L': return DateFormat.LONG; - case 'F': return DateFormat.FULL; - case '-': return -1; - } + char ch = this.stylePattern.charAt(index); + return switch (ch) { + case 'S' -> DateFormat.SHORT; + case 'M' -> DateFormat.MEDIUM; + case 'L' -> DateFormat.LONG; + case 'F' -> DateFormat.FULL; + case '-' -> -1; + default -> throw unsupportedStylePatternException(); + }; } - throw new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); + throw unsupportedStylePatternException(); + } + + private IllegalStateException unsupportedStylePatternException() { + return new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); } } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java index 35f361fe6547..2c165c5c0feb 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ /** * Configures basic date formatting for use with Spring, primarily for * {@link org.springframework.format.annotation.DateTimeFormat} declarations. - * Applies to fields of type {@link Date}, {@link Calendar} and {@code long}. + * Applies to fields of type {@link Date}, {@link Calendar}, and {@code long}. * *

Designed for direct instantiation but also exposes the static * {@link #addDateConverters(ConverterRegistry)} utility method for diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java index a526f23d9be5..1deaa6d60154 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ * @author Sam Brannen * @since 3.2 */ -public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport +public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport implements AnnotationFormatterFactory { private static final Set> FIELD_TYPES = Set.of(Date.class, Calendar.class, Long.class); diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java index 1a72d4494938..3a340be6ba00 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,8 +76,8 @@ public DateTimeFormatterRegistrar() { /** * Set whether standard ISO formatting should be applied to all date/time types. - * Default is "false" (no). - *

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

Default is "false" (no). + *

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

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

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

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

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

This formatter will be used for {@link LocalDateTime}, {@link ZonedDateTime}, * and {@link OffsetDateTime} types. When specified, the * {@link #setDateTimeStyle dateTimeStyle} and * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java index be767f0e23c9..3aff5d779dd2 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,10 +29,18 @@ */ abstract class DateTimeFormatterUtils { + /** + * Create a {@link DateTimeFormatter} for the supplied pattern, configured with + * {@linkplain ResolverStyle#STRICT strict} resolution. + *

Note that the strict resolution does not affect the parsing. + * @param pattern the pattern to use + * @return a new {@code DateTimeFormatter} + * @see ResolverStyle#STRICT + */ static DateTimeFormatter createStrictDateTimeFormatter(String pattern) { - // Using strict parsing to align with Joda-Time and standard DateFormat behavior: + // Using strict resolution to align with Joda-Time and standard DateFormat behavior: // otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected. - // However, with strict parsing, a year digit needs to be specified as 'u'... + // However, with strict resolution, a year digit needs to be specified as 'u'... String patternToUse = StringUtils.replace(pattern, "yy", "uu"); return DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT); } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 456c0ad09090..e40cb3174a76 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,13 +41,18 @@ public class InstantFormatter implements Formatter { @Override public Instant parse(String text, Locale locale) throws ParseException { - if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) { - // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" - return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); + try { + return Instant.ofEpochMilli(Long.parseLong(text)); } - else { - // assuming UTC instant a la "2007-12-03T10:15:30.00Z" - return Instant.parse(text); + catch (NumberFormatException ex) { + if (!text.isEmpty() && Character.isAlphabetic(text.charAt(0))) { + // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" + return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); + } + else { + // assuming UTC instant a la "2007-12-03T10:15:30.000Z" + return Instant.parse(text); + } } } diff --git a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java index 32845e4c3b33..aa3ecdbdbd68 100644 --- a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,8 @@ * as {@code DefaultConversionService} exposes its own * {@link DefaultConversionService#addDefaultConverters addDefaultConverters} method. * - *

Automatically registers formatters for JSR-354 Money & Currency, JSR-310 Date-Time - * and/or Joda-Time 2.x, depending on the presence of the corresponding API on the classpath. + *

Automatically registers formatters for JSR-354 Money & Currency and JSR-310 Date-Time + * depending on the presence of the corresponding API on the classpath. * * @author Chris Beams * @author Juergen Hoeller diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceRuntimeHints.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceRuntimeHints.java new file mode 100644 index 000000000000..d1f41d84fcb3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceRuntimeHints.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.format.support; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.Nullable; + +/** + * {@link RuntimeHintsRegistrar} to register hints for {@link DefaultFormattingConversionService}. + * + * @author Brian Clozel + * @since 6.1 + */ +class FormattingConversionServiceRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + hints.reflection().registerType(TypeReference.of("javax.money.MonetaryAmount")); + } +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java index 03db47b6b473..f0a51d719be1 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java @@ -85,7 +85,7 @@ public byte[] transformIfNecessary(String className, byte[] bytes) { * @param className the full qualified name of the class in dot format (i.e. some.package.SomeClass) * @param internalName class name internal name in / format (i.e. some/package/SomeClass) * @param bytes class byte definition - * @param pd protection domain to be used (can be null) + * @param pd protection domain to be used (can be {@code null}) * @return (possibly transformed) class byte definition */ public byte[] transformIfNecessary(String className, String internalName, byte[] bytes, @Nullable ProtectionDomain pd) { diff --git a/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java b/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java index ea16a2fed873..2cd42366d217 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java @@ -18,6 +18,8 @@ import javax.management.JMRuntimeException; +import org.springframework.lang.Nullable; + /** * Thrown when trying to invoke an operation on a proxy that is not exposed * by the proxied MBean resource's management interface. @@ -35,7 +37,7 @@ public class InvalidInvocationException extends JMRuntimeException { * error message. * @param msg the detail message */ - public InvalidInvocationException(String msg) { + public InvalidInvocationException(@Nullable String msg) { super(msg); } diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java index 13a10ecec14f..1142cfa943e3 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java @@ -348,7 +348,7 @@ protected boolean isPrepared() { /** - * Route the invocation to the configured managed resource.. + * Route the invocation to the configured managed resource. * @param invocation the {@code MethodInvocation} to re-route * @return the value returned as a result of the re-routed invocation * @throws Throwable an invocation error propagated to the user @@ -605,8 +605,8 @@ else if (Collection.class.isAssignableFrom(targetClass)) { } private Object convertDataArrayToTargetArray(Object[] array, Class targetClass) throws NoSuchMethodException { - Class targetType = targetClass.getComponentType(); - Method fromMethod = targetType.getMethod("from", array.getClass().getComponentType()); + Class targetType = targetClass.componentType(); + Method fromMethod = targetType.getMethod("from", array.getClass().componentType()); Object resultArray = Array.newInstance(targetType, array.length); for (int i = 0; i < array.length; i++) { Array.set(resultArray, i, ReflectionUtils.invokeMethod(fromMethod, null, array[i])); @@ -617,7 +617,7 @@ private Object convertDataArrayToTargetArray(Object[] array, Class targetClas private Collection convertDataArrayToTargetCollection(Object[] array, Class collectionType, Class elementType) throws NoSuchMethodException { - Method fromMethod = elementType.getMethod("from", array.getClass().getComponentType()); + Method fromMethod = elementType.getMethod("from", array.getClass().componentType()); Collection resultColl = CollectionFactory.createCollection(collectionType, Array.getLength(array)); for (Object element : array) { resultColl.add(ReflectionUtils.invokeMethod(fromMethod, null, element)); diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java index f55de8e37b88..b0c62aa13a17 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,18 +83,21 @@ public void setBeanClassLoader(ClassLoader classLoader) { public void afterPropertiesSet() throws MBeanServerNotFoundException, MBeanInfoRetrievalException { super.afterPropertiesSet(); + Class interfaceToUse; if (this.proxyInterface == null) { - this.proxyInterface = getManagementInterface(); - if (this.proxyInterface == null) { + interfaceToUse = getManagementInterface(); + if (interfaceToUse == null) { throw new IllegalArgumentException("Property 'proxyInterface' or 'managementInterface' is required"); } + this.proxyInterface = interfaceToUse; } else { + interfaceToUse = this.proxyInterface; if (getManagementInterface() == null) { - setManagementInterface(this.proxyInterface); + setManagementInterface(interfaceToUse); } } - this.mbeanProxy = new ProxyFactory(this.proxyInterface, this).getProxy(this.beanClassLoader); + this.mbeanProxy = new ProxyFactory(interfaceToUse, this).getProxy(this.beanClassLoader); } @@ -105,6 +108,7 @@ public Object getObject() { } @Override + @Nullable public Class getObjectType() { return this.proxyInterface; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java index dd72cc1581fa..50aef5f21386 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -75,7 +75,7 @@ * JMX-specific information in the bean classes. * *

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

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

This feature is turned off by default. Explicitly specify - * {@code true} here to enable autodetection. + * {@code true} here to enable auto-detection. * @see #setAssembler * @see AutodetectCapableMBeanInfoAssembler * @see #isMBean @@ -228,7 +236,7 @@ public void setAutodetect(boolean autodetect) { } /** - * Set the autodetection mode to use by name. + * Set the auto-detection mode to use by name. * @throws IllegalArgumentException if the supplied value is not resolvable * to one of the {@code AUTODETECT_} constants or is {@code null} * @see #setAutodetectMode(int) @@ -236,7 +244,9 @@ public void setAutodetect(boolean autodetect) { * @see #AUTODETECT_ASSEMBLER * @see #AUTODETECT_MBEAN * @see #AUTODETECT_NONE + * @deprecated as of 6.1, in favor of the {@link #setAutodetect "autodetect" flag} */ + @Deprecated(since = "6.1") public void setAutodetectModeName(String constantName) { Assert.hasText(constantName, "'constantName' must not be null or blank"); Integer mode = constants.get(constantName); @@ -245,7 +255,7 @@ public void setAutodetectModeName(String constantName) { } /** - * Set the autodetection mode to use. + * Set the auto-detection mode to use. * @throws IllegalArgumentException if the supplied value is not * one of the {@code AUTODETECT_} constants * @see #setAutodetectModeName(String) @@ -253,7 +263,9 @@ public void setAutodetectModeName(String constantName) { * @see #AUTODETECT_ASSEMBLER * @see #AUTODETECT_MBEAN * @see #AUTODETECT_NONE + * @deprecated as of 6.1, in favor of the {@link #setAutodetect "autodetect" flag} */ + @Deprecated(since = "6.1") public void setAutodetectMode(int autodetectMode) { Assert.isTrue(constants.containsValue(autodetectMode), "Only values of autodetect constants allowed"); @@ -262,7 +274,7 @@ public void setAutodetectMode(int autodetectMode) { /** * Specify whether to allow eager initialization of candidate beans - * when autodetecting MBeans in the Spring application context. + * when auto-detecting MBeans in the Spring application context. *

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

The passed-in assembler can optionally implement the * {@code AutodetectCapableMBeanInfoAssembler} interface, which enables it - * to participate in the exporter's MBean autodetection process. + * to participate in the exporter's MBean auto-detection process. * @see org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler * @see org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler @@ -322,7 +334,7 @@ public void setExposeManagedResourceClassLoader(boolean exposeManagedResourceCla } /** - * Set the list of names for beans that should be excluded from autodetection. + * Set the list of names for beans that should be excluded from auto-detection. */ public void setExcludedBeans(String... excludedBeans) { this.excludedBeans.clear(); @@ -330,7 +342,7 @@ public void setExcludedBeans(String... excludedBeans) { } /** - * Add the name of bean that should be excluded from autodetection. + * Add the name of bean that should be excluded from auto-detection. */ public void addExcludedBean(String excludedBean) { Assert.notNull(excludedBean, "ExcludedBean must not be null"); @@ -399,7 +411,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { /** * This callback is only required for resolution of bean names in the * {@link #setBeans(java.util.Map) "beans"} {@link Map} and for - * autodetection of MBeans (in the latter case, a + * auto-detection of MBeans (in the latter case, a * {@code ListableBeanFactory} is required). * @see #setBeans * @see #setAutodetect @@ -410,7 +422,7 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = lbf; } else { - logger.debug("MBeanExporter not running in a ListableBeanFactory: autodetection of MBeans not available."); + logger.debug("MBeanExporter not running in a ListableBeanFactory: auto-detection of MBeans not available."); } } @@ -525,7 +537,7 @@ public void unregisterManagedResource(ObjectName objectName) { * implementation of the {@code ObjectNamingStrategy} interface being used. */ protected void registerBeans() { - // The beans property may be null, for example if we are relying solely on autodetection. + // The beans property may be null, for example if we are relying solely on auto-detection. if (this.beans == null) { this.beans = new HashMap<>(); // Use AUTODETECT_ALL as default in no beans specified explicitly. @@ -534,7 +546,7 @@ protected void registerBeans() { } } - // Perform autodetection, if desired. + // Perform auto-detection, if desired. int mode = (this.autodetectMode != null ? this.autodetectMode : AUTODETECT_NONE); if (mode != AUTODETECT_NONE) { if (this.beanFactory == null) { @@ -542,7 +554,7 @@ protected void registerBeans() { } if (mode == AUTODETECT_MBEAN || mode == AUTODETECT_ALL) { // Autodetect any beans that are already MBeans. - logger.debug("Autodetecting user-defined JMX MBeans"); + logger.debug("Auto-detecting user-defined JMX MBeans"); autodetect(this.beans, (beanClass, beanName) -> isMBean(beanClass)); } // Allow the assembler a chance to vote for bean inclusion. @@ -859,11 +871,11 @@ private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws J //--------------------------------------------------------------------- - // Autodetection process + // auto-detection process //--------------------------------------------------------------------- /** - * Performs the actual autodetection process, delegating to an + * Performs the actual auto-detection process, delegating to an * {@code AutodetectCallback} instance to vote on the inclusion of a * given bean. * @param callback the {@code AutodetectCallback} to use when deciding @@ -1062,13 +1074,13 @@ private void notifyListenersOfUnregistration(ObjectName objectName) { //--------------------------------------------------------------------- /** - * Internal callback interface for the autodetection process. + * Internal callback interface for the auto-detection process. */ @FunctionalInterface private interface AutodetectCallback { /** - * Called during the autodetection process to decide whether + * Called during the auto-detection process to decide whether * a bean should be included. * @param beanClass the class of the bean * @param beanName the name of the bean diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java index 975b40be7c59..6f02406bb164 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java @@ -117,7 +117,7 @@ public org.springframework.jmx.export.metadata.ManagedAttribute getManagedAttrib pvs.removePropertyValue("defaultValue"); PropertyAccessorFactory.forBeanPropertyAccess(bean).setPropertyValues(pvs); String defaultValue = (String) map.get("defaultValue"); - if (defaultValue.length() > 0) { + if (!defaultValue.isEmpty()) { bean.setDefaultValue(defaultValue); } return bean; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java index f0951d11b42f..7d77c17793bb 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,11 +27,13 @@ * {@link ManagedResource}, {@link ManagedAttribute}, {@link ManagedOperation}, etc. * *

Sets a {@link MetadataNamingStrategy} and a {@link MetadataMBeanInfoAssembler} - * with an {@link AnnotationJmxAttributeSource}, and activates the - * {@link #AUTODETECT_ALL} mode by default. + * with an {@link AnnotationJmxAttributeSource}, and activates + * {@link #setAutodetect autodetection} by default. * * @author Juergen Hoeller * @since 2.5 + * @see #setAutodetect + * @see AnnotationJmxAttributeSource */ public class AnnotationMBeanExporter extends MBeanExporter { @@ -48,7 +50,7 @@ public class AnnotationMBeanExporter extends MBeanExporter { public AnnotationMBeanExporter() { setNamingStrategy(this.metadataNamingStrategy); setAssembler(this.metadataAssembler); - setAutodetectMode(AUTODETECT_ALL); + setAutodetect(true); } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java index a033c01833fd..38519552fdb9 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.springframework.jmx.export.assembler; /** - * Extends the {@code MBeanInfoAssembler} to add autodetection logic. + * Extends the {@code MBeanInfoAssembler} to add auto-detection logic. * Implementations of this interface are given the opportunity by the * {@code MBeanExporter} to include additional beans in the registration process. * diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java index a174e5870bde..86d0cca923e2 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -118,7 +118,7 @@ protected void checkManagedBean(Object managedBean) throws IllegalArgumentExcept } /** - * Used for autodetection of beans. Checks to see if the bean's class has a + * Used for auto-detection of beans. Checks to see if the bean's class has a * {@code ManagedResource} attribute. If so, it will add it to the list of included beans. * @param beanClass the class of the bean * @param beanName the name of the bean in the bean factory @@ -417,7 +417,7 @@ protected void populateOperationDescriptor(Descriptor desc, Method method, Strin * @param setter the int associated with the setter for this attribute */ private int resolveIntDescriptor(int getter, int setter) { - return (getter >= setter ? getter : setter); + return Math.max(getter, setter); } /** diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java index c0a2c4d875e6..ea4792a14b51 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,9 @@ */ public class MetadataNamingStrategy implements ObjectNamingStrategy, InitializingBean { + private static final char[] QUOTABLE_CHARS = new char[] {',', '=', ':', '"'}; + + /** * The {@code JmxAttributeSource} implementation to use for reading metadata. */ @@ -132,10 +135,23 @@ public ObjectName getObjectName(Object managedBean, @Nullable String beanKey) th } Hashtable properties = new Hashtable<>(); properties.put("type", ClassUtils.getShortName(managedClass)); - properties.put("name", beanKey); + properties.put("name", quoteIfNecessary(beanKey)); return ObjectNameManager.getInstance(domain, properties); } } } + private static String quoteIfNecessary(String value) { + return shouldQuote(value) ? ObjectName.quote(value) : value; + } + + private static boolean shouldQuote(String value) { + for (char quotableChar : QUOTABLE_CHARS) { + if (value.indexOf(quotableChar) != -1) { + return true; + } + } + return false; + } + } diff --git a/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java index 23b45e3f7c16..43cb7e719ace 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -169,7 +169,7 @@ public static Class[] parameterInfoToTypes( /** * Create a {@code String[]} representing the argument signature of a * method. Each element in the array is the fully qualified class name - * of the corresponding argument in the methods signature. + * of the corresponding argument in the method's signature. * @param method the method to build an argument signature for * @return the signature as array of argument types */ diff --git a/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java b/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java index 6866af037101..9244be32ced0 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java @@ -167,11 +167,8 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(this.notificationListener); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.notificationFilter); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.handback); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.mappedObjectNames); - return hashCode; + return ObjectUtils.nullSafeHash(this.notificationListener, this.notificationFilter, + this.handback, this.mappedObjectNames); } } diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java index a237e6c731ef..b56ce01e0bca 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java @@ -273,6 +273,7 @@ public Object getObject() { } @Override + @Nullable public Class getObjectType() { if (this.proxyInterfaces != null) { if (this.proxyInterfaces.length == 1) { diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java index 21b1e0cdae65..cc8e392a7778 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -148,8 +148,4 @@ public Object getTarget() { } } - @Override - public void releaseTarget(Object target) { - } - } diff --git a/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java index 09a54d2f44d3..e1b2349ea84b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.scheduling; +import org.springframework.lang.Nullable; + /** * Extension of the {@link Runnable} interface, adding special callbacks * for long-running operations. @@ -38,7 +40,27 @@ public interface SchedulingAwareRunnable extends Runnable { * pool (if any) but rather be considered as long-running background thread. *

This should be considered a hint. Of course TaskExecutor implementations * are free to ignore this flag and the SchedulingAwareRunnable interface overall. + *

The default implementation returns {@code false}, as of 6.1. + */ + default boolean isLongLived() { + return false; + } + + /** + * Return a qualifier associated with this Runnable. + *

The default implementation returns {@code null}. + *

May be used for custom purposes depending on the scheduler implementation. + * {@link org.springframework.scheduling.config.TaskSchedulerRouter} introspects + * this qualifier in order to determine the target scheduler to be used + * for a given Runnable, matching the qualifier value (or the bean name) + * of a specific {@link org.springframework.scheduling.TaskScheduler} or + * {@link java.util.concurrent.ScheduledExecutorService} bean definition. + * @since 6.1 + * @see org.springframework.scheduling.annotation.Scheduled#scheduler() */ - boolean isLongLived(); + @Nullable + default String getQualifier() { + return null; + } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java index 00cb01282cc3..a9a9d4949a56 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,9 +68,9 @@ default Clock getClock() { * @param trigger an implementation of the {@link Trigger} interface, * e.g. a {@link org.springframework.scheduling.support.CronTrigger} object * wrapping a cron expression - * @return a {@link ScheduledFuture} representing pending completion of the task, + * @return a {@link ScheduledFuture} representing pending execution of the task, * or {@code null} if the given Trigger object never fires (i.e. returns - * {@code null} from {@link Trigger#nextExecutionTime}) + * {@code null} from {@link Trigger#nextExecution}) * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @see org.springframework.scheduling.support.CronTrigger @@ -85,7 +85,7 @@ default Clock getClock() { * @param task the Runnable to execute whenever the trigger fires * @param startTime the desired execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @since 5.0 @@ -99,7 +99,7 @@ default Clock getClock() { * @param task the Runnable to execute whenever the trigger fires * @param startTime the desired execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #schedule(Runnable, Instant)} @@ -118,7 +118,7 @@ default ScheduledFuture schedule(Runnable task, Date startTime) { * @param startTime the desired first execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param period the interval between successive executions of the task - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @since 5.0 @@ -134,7 +134,7 @@ default ScheduledFuture schedule(Runnable task, Date startTime) { * @param startTime the desired first execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param period the interval between successive executions of the task (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleAtFixedRate(Runnable, Instant, Duration)} @@ -151,7 +151,7 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, lo * {@link ScheduledFuture} gets cancelled. * @param task the Runnable to execute whenever the trigger fires * @param period the interval between successive executions of the task - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @since 5.0 @@ -165,7 +165,7 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, lo * {@link ScheduledFuture} gets cancelled. * @param task the Runnable to execute whenever the trigger fires * @param period the interval between successive executions of the task (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleAtFixedRate(Runnable, Duration)} @@ -185,7 +185,7 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { * @param startTime the desired first execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param delay the delay between the completion of one execution and the start of the next - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @since 5.0 @@ -203,7 +203,7 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param delay the delay between the completion of one execution and the start of the next * (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleWithFixedDelay(Runnable, Instant, Duration)} @@ -220,7 +220,7 @@ default ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, * {@link ScheduledFuture} gets cancelled. * @param task the Runnable to execute whenever the trigger fires * @param delay the delay between the completion of one execution and the start of the next - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @since 5.0 @@ -235,7 +235,7 @@ default ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, * @param task the Runnable to execute whenever the trigger fires * @param delay the delay between the completion of one execution and the start of the next * (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleWithFixedDelay(Runnable, Duration)} diff --git a/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java index d70159b14f0b..d2fc96d677d0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java @@ -52,7 +52,7 @@ default Clock getClock() { @Deprecated(since = "6.0") default Date lastScheduledExecutionTime() { Instant instant = lastScheduledExecution(); - return instant != null ? Date.from(instant) : null; + return (instant != null ? Date.from(instant) : null); } /** @@ -73,7 +73,7 @@ default Date lastScheduledExecutionTime() { @Deprecated(since = "6.0") default Date lastActualExecutionTime() { Instant instant = lastActualExecution(); - return instant != null ? Date.from(instant) : null; + return (instant != null ? Date.from(instant) : null); } /** @@ -94,7 +94,7 @@ default Date lastActualExecutionTime() { @Nullable default Date lastCompletionTime() { Instant instant = lastCompletion(); - return instant != null ? Date.from(instant) : null; + return (instant != null ? Date.from(instant) : null); } /** diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java index f20aba5585d4..9d8f801590eb 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author 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,8 +98,8 @@ public AsyncAnnotationBeanPostProcessor() { * applying the corresponding default if a supplier is not resolvable. * @since 5.1 */ - public void configure( - @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + public void configure(@Nullable Supplier executor, + @Nullable Supplier exceptionHandler) { this.executor = executor; this.exceptionHandler = exceptionHandler; diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java index bf7a2d0baf63..4c1e83146873 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java @@ -34,6 +34,7 @@ public class AsyncConfigurerSupport implements AsyncConfigurer { @Override + @Nullable public Executor getAsyncExecutor() { return null; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 41e88d3f50bf..ed0956779138 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,18 +28,33 @@ import org.springframework.scheduling.config.ScheduledTaskRegistrar; /** - * Annotation that marks a method to be scheduled. Exactly one of the - * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes - * must be specified. + * Annotation that marks a method to be scheduled. For periodic tasks, exactly one + * of the {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes + * must be specified, and additionally an optional {@link #initialDelay}. + * For a one-time task, it is sufficient to just specify an {@link #initialDelay}. * - *

The annotated method must expect no arguments. It will typically have + *

The annotated method must not accept arguments. It will typically have * a {@code void} return type; if not, the returned value will be ignored * when called through the scheduler. * - *

Processing of {@code @Scheduled} annotations is performed by - * registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be - * done manually or, more conveniently, through the {@code } - * XML element or {@link EnableScheduling @EnableScheduling} annotation. + *

Methods that return a reactive {@code Publisher} or a type which can be adapted + * to {@code Publisher} by the default {@code ReactiveAdapterRegistry} are supported. + * The {@code Publisher} must support multiple subsequent subscriptions. The returned + * {@code Publisher} is only produced once, and the scheduling infrastructure then + * periodically subscribes to it according to configuration. Values emitted by + * the publisher are ignored. Errors are logged at {@code WARN} level, which + * doesn't prevent further iterations. If a fixed delay is configured, the + * subscription is blocked in order to respect the fixed delay semantics. + * + *

Kotlin suspending functions are also supported, provided the coroutine-reactor + * bridge ({@code kotlinx.coroutine.reactor}) is present at runtime. This bridge is + * used to adapt the suspending function to a {@code Publisher} which is treated + * the same way as in the reactive method case (see above). + * + *

Processing of {@code @Scheduled} annotations is performed by registering a + * {@link ScheduledAnnotationBeanPostProcessor}. This can be done manually or, + * more conveniently, through the {@code } XML element + * or {@link EnableScheduling @EnableScheduling} annotation. * *

This annotation can be used as a {@linkplain Repeatable repeatable} * annotation. If several scheduled declarations are found on the same method, @@ -102,9 +117,9 @@ /** * A time zone for which the cron expression will be resolved. By default, this - * attribute is the empty String (i.e. the server's local time zone will be used). + * attribute is the empty String (i.e. the scheduler's time zone will be used). * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)}, - * or an empty String to indicate the server's default time zone + * or an empty String to indicate the scheduler's default time zone * @since 4.0 * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone) * @see java.util.TimeZone @@ -112,48 +127,54 @@ String zone() default ""; /** - * Execute the annotated method with a fixed period between the end of the - * last invocation and the start of the next. + * Execute the annotated method with a fixed period between invocations. *

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

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

This attribute variant supports Spring-style "${...}" placeholders * as well as SpEL expressions. - * @return the delay as a String value — for example, a placeholder + * @return the period as a String value — for example, a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 - * @see #fixedDelay() + * @see #fixedRate() */ - String fixedDelayString() default ""; + String fixedRateString() default ""; /** - * Execute the annotated method with a fixed period between invocations. + * Execute the annotated method with a fixed period between the end of the + * last invocation and the start of the next. *

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

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

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

This attribute variant supports Spring-style "${...}" placeholders * as well as SpEL expressions. - * @return the period as a String value — for example, a placeholder + *

NOTE: With virtual threads, fixed rates and cron triggers are recommended + * over fixed delays. Fixed-delay tasks operate on a single scheduler thread + * with {@link org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler}. + * @return the delay as a String value — for example, a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 - * @see #fixedRate() + * @see #fixedDelay() */ - String fixedRateString() default ""; + String fixedDelayString() default ""; /** * Number of units of time to delay before the first execution of a @@ -192,4 +213,16 @@ */ TimeUnit timeUnit() default TimeUnit.MILLISECONDS; + /** + * A qualifier for determining a scheduler to run this scheduled method on. + *

Defaults to an empty String, suggesting the default scheduler. + *

May be used to determine the target scheduler to be used, + * matching the qualifier value (or the bean name) of a specific + * {@link org.springframework.scheduling.TaskScheduler} or + * {@link java.util.concurrent.ScheduledExecutorService} bean definition. + * @since 6.1 + * @see org.springframework.scheduling.SchedulingAwareRunnable#getQualifier() + */ + String scheduler() default ""; + } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index 07075d9688a8..78701f901f5e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,8 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -42,19 +42,17 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.SmartInitializingSingleton; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; -import org.springframework.beans.factory.config.NamedBeanHolder; +import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.context.event.ApplicationContextEvent; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.MethodIntrospector; import org.springframework.core.Ordered; @@ -67,12 +65,15 @@ import org.springframework.scheduling.config.CronTask; import org.springframework.scheduling.config.FixedDelayTask; import org.springframework.scheduling.config.FixedRateTask; +import org.springframework.scheduling.config.OneTimeTask; import org.springframework.scheduling.config.ScheduledTask; import org.springframework.scheduling.config.ScheduledTaskHolder; import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.config.TaskSchedulerRouter; import org.springframework.scheduling.support.CronTrigger; import org.springframework.scheduling.support.ScheduledMethodRunnable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -98,6 +99,7 @@ * @author Elizabeth Chatman * @author Victor Brown * @author Sam Brannen + * @author Simon Baslé * @since 3.0 * @see Scheduled * @see EnableScheduling @@ -109,7 +111,7 @@ public class ScheduledAnnotationBeanPostProcessor implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor, Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware, - SmartInitializingSingleton, ApplicationListener, DisposableBean { + SmartInitializingSingleton, DisposableBean, ApplicationListener { /** * The default name of the {@link TaskScheduler} bean to pick up: {@value}. @@ -117,9 +119,15 @@ public class ScheduledAnnotationBeanPostProcessor * in case of multiple scheduler beans found in the context. * @since 4.2 */ - public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; + public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = TaskSchedulerRouter.DEFAULT_TASK_SCHEDULER_BEAN_NAME; + /** + * Reactive Streams API present on the classpath? + */ + private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + "org.reactivestreams.Publisher", ScheduledAnnotationBeanPostProcessor.class.getClassLoader()); + protected final Log logger = LogFactory.getLog(getClass()); private final ScheduledTaskRegistrar registrar; @@ -139,10 +147,17 @@ public class ScheduledAnnotationBeanPostProcessor @Nullable private ApplicationContext applicationContext; + @Nullable + private TaskSchedulerRouter localScheduler; + private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); private final Map> scheduledTasks = new IdentityHashMap<>(16); + private final Map> reactiveSubscriptions = new IdentityHashMap<>(16); + + private final Set manualCancellationOnContextClose = Collections.newSetFromMap(new IdentityHashMap<>(16)); + /** * Create a default {@code ScheduledAnnotationBeanPostProcessor}. @@ -229,20 +244,16 @@ public void afterSingletonsInstantiated() { } } - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - if (event.getApplicationContext() == this.applicationContext) { - // Running in an ApplicationContext -> register tasks this late... - // giving other ContextRefreshedEvent listeners a chance to perform - // their work at the same time (e.g. Spring Batch's job registration). - finishRegistration(); - } - } - private void finishRegistration() { if (this.scheduler != null) { this.registrar.setScheduler(this.scheduler); } + else { + this.localScheduler = new TaskSchedulerRouter(); + this.localScheduler.setBeanName(this.beanName); + this.localScheduler.setBeanFactory(this.beanFactory); + this.registrar.setTaskScheduler(this.localScheduler); + } if (this.beanFactory instanceof ListableBeanFactory lbf) { Map beans = lbf.getBeansOfType(SchedulingConfigurer.class); @@ -253,91 +264,9 @@ private void finishRegistration() { } } - if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) { - Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type"); - try { - // Search for TaskScheduler bean... - this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false)); - } - catch (NoUniqueBeanDefinitionException ex) { - if (logger.isTraceEnabled()) { - logger.trace("Could not find unique TaskScheduler bean - attempting to resolve by name: " + - ex.getMessage()); - } - try { - this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true)); - } - catch (NoSuchBeanDefinitionException ex2) { - if (logger.isInfoEnabled()) { - logger.info("More than one TaskScheduler bean exists within the context, and " + - "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + - "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + - "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + - ex.getBeanNamesFound()); - } - } - } - catch (NoSuchBeanDefinitionException ex) { - if (logger.isTraceEnabled()) { - logger.trace("Could not find default TaskScheduler bean - attempting to find ScheduledExecutorService: " + - ex.getMessage()); - } - // Search for ScheduledExecutorService bean next... - try { - this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false)); - } - catch (NoUniqueBeanDefinitionException ex2) { - if (logger.isTraceEnabled()) { - logger.trace("Could not find unique ScheduledExecutorService bean - attempting to resolve by name: " + - ex2.getMessage()); - } - try { - this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true)); - } - catch (NoSuchBeanDefinitionException ex3) { - if (logger.isInfoEnabled()) { - logger.info("More than one ScheduledExecutorService bean exists within the context, and " + - "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + - "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + - "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + - ex2.getBeanNamesFound()); - } - } - } - catch (NoSuchBeanDefinitionException ex2) { - if (logger.isTraceEnabled()) { - logger.trace("Could not find default ScheduledExecutorService bean - falling back to default: " + - ex2.getMessage()); - } - // Giving up -> falling back to default scheduler within the registrar... - logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing"); - } - } - } - this.registrar.afterPropertiesSet(); } - private T resolveSchedulerBean(BeanFactory beanFactory, Class schedulerType, boolean byName) { - if (byName) { - T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType); - if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory cbf) { - cbf.registerDependentBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName); - } - return scheduler; - } - else if (beanFactory instanceof AutowireCapableBeanFactory acbf) { - NamedBeanHolder holder = acbf.resolveNamedBean(schedulerType); - if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory cbf) { - cbf.registerDependentBean(holder.getBeanName(), this.beanName); - } - return holder.getBeanInstance(); - } - else { - return beanFactory.getBean(schedulerType); - } - } - @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { @@ -379,21 +308,100 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + "': " + annotatedMethods); } + if ((this.beanFactory != null && + (!this.beanFactory.containsBean(beanName) || !this.beanFactory.isSingleton(beanName)) || + (this.beanFactory instanceof SingletonBeanRegistry sbr && sbr.containsSingleton(beanName)))) { + // Either a prototype/scoped bean or a FactoryBean with a pre-existing managed singleton + // -> trigger manual cancellation when ContextClosedEvent comes in + this.manualCancellationOnContextClose.add(bean); + } } } return bean; } /** - * Process the given {@code @Scheduled} method declaration on the given bean. + * Process the given {@code @Scheduled} method declaration on the given bean, + * attempting to distinguish {@linkplain #processScheduledAsync(Scheduled, Method, Object) + * reactive} methods from {@linkplain #processScheduledSync(Scheduled, Method, Object) + * synchronous} methods. * @param scheduled the {@code @Scheduled} annotation * @param method the method that the annotation has been declared on * @param bean the target bean instance - * @see #createRunnable(Object, Method) + * @see #processScheduledSync(Scheduled, Method, Object) + * @see #processScheduledAsync(Scheduled, Method, Object) */ protected void processScheduled(Scheduled scheduled, Method method, Object bean) { + // Is the method a Kotlin suspending function? Throws if true and the reactor bridge isn't on the classpath. + // Does the method return a reactive type? Throws if true and it isn't a deferred Publisher type. + if (reactiveStreamsPresent && ScheduledAnnotationReactiveSupport.isReactive(method)) { + processScheduledAsync(scheduled, method, bean); + return; + } + processScheduledSync(scheduled, method, bean); + } + + /** + * Process the given {@code @Scheduled} method declaration on the given bean, + * as a synchronous method. The method must accept no arguments. Its return value + * is ignored (if any), and the scheduled invocations of the method take place + * using the underlying {@link TaskScheduler} infrastructure. + * @param scheduled the {@code @Scheduled} annotation + * @param method the method that the annotation has been declared on + * @param bean the target bean instance + */ + private void processScheduledSync(Scheduled scheduled, Method method, Object bean) { + Runnable task; + try { + task = createRunnable(bean, method, scheduled.scheduler()); + } + catch (IllegalArgumentException ex) { + throw new IllegalStateException("Could not create recurring task for @Scheduled method '" + + method.getName() + "': " + ex.getMessage()); + } + processScheduledTask(scheduled, task, method, bean); + } + + /** + * Process the given {@code @Scheduled} bean method declaration which returns + * a {@code Publisher}, or the given Kotlin suspending function converted to a + * {@code Publisher}. A {@code Runnable} which subscribes to that publisher is + * then repeatedly scheduled according to the annotation configuration. + *

Note that for fixed delay configuration, the subscription is turned into a blocking + * call instead. Types for which a {@code ReactiveAdapter} is registered but which cannot + * be deferred (i.e. not a {@code Publisher}) are not supported. + * @param scheduled the {@code @Scheduled} annotation + * @param method the method that the annotation has been declared on, which + * must either return a Publisher-adaptable type or be a Kotlin suspending function + * @param bean the target bean instance + * @see ScheduledAnnotationReactiveSupport + */ + private void processScheduledAsync(Scheduled scheduled, Method method, Object bean) { + Runnable task; + try { + task = ScheduledAnnotationReactiveSupport.createSubscriptionRunnable(method, bean, scheduled, + this.registrar::getObservationRegistry, + this.reactiveSubscriptions.computeIfAbsent(bean, k -> new CopyOnWriteArrayList<>())); + } + catch (IllegalArgumentException ex) { + throw new IllegalStateException("Could not create recurring task for @Scheduled method '" + + method.getName() + "': " + ex.getMessage()); + } + processScheduledTask(scheduled, task, method, bean); + } + + /** + * Parse the {@code Scheduled} annotation and schedule the provided {@code Runnable} + * accordingly. The Runnable can represent either a synchronous method invocation + * (see {@link #processScheduledSync(Scheduled, Method, Object)}) or an asynchronous + * one (see {@link #processScheduledAsync(Scheduled, Method, Object)}). + * @param scheduled the {@code @Scheduled} annotation + * @param runnable the runnable to be scheduled + * @param method the method that the annotation has been declared on + * @param bean the target bean instance + */ + private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) { try { - Runnable runnable = createRunnable(bean, method); boolean processedSchedule = false; String errorMessage = "Exactly one of the 'cron', 'fixedDelay' or 'fixedRate' attributes is required"; @@ -413,7 +421,7 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) } catch (RuntimeException ex) { throw new IllegalArgumentException( - "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long"); + "Invalid initialDelayString value \"" + initialDelayString + "\"; " + ex); } } } @@ -430,29 +438,27 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) Assert.isTrue(initialDelay.isNegative(), "'initialDelay' not supported for cron triggers"); processedSchedule = true; if (!Scheduled.CRON_DISABLED.equals(cron)) { - TimeZone timeZone; + CronTrigger trigger; if (StringUtils.hasText(zone)) { - timeZone = StringUtils.parseTimeZoneString(zone); + trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone)); } else { - timeZone = TimeZone.getDefault(); + trigger = new CronTrigger(cron); } - tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger))); } } } // At this point we don't need to differentiate between initial delay set or not anymore - if (initialDelay.isNegative()) { - initialDelay = Duration.ZERO; - } + Duration delayToUse = (initialDelay.isNegative() ? Duration.ZERO : initialDelay); // Check fixed delay Duration fixedDelay = toDuration(scheduled.fixedDelay(), scheduled.timeUnit()); if (!fixedDelay.isNegative()) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); + tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse))); } String fixedDelayString = scheduled.fixedDelayString(); if (StringUtils.hasText(fixedDelayString)) { @@ -467,9 +473,9 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) } catch (RuntimeException ex) { throw new IllegalArgumentException( - "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long"); + "Invalid fixedDelayString value \"" + fixedDelayString + "\"; " + ex); } - tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); + tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse))); } } @@ -478,7 +484,7 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) if (!fixedRate.isNegative()) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay))); + tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse))); } String fixedRateString = scheduled.fixedRateString(); if (StringUtils.hasText(fixedRateString)) { @@ -493,14 +499,18 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) } catch (RuntimeException ex) { throw new IllegalArgumentException( - "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long"); + "Invalid fixedRateString value \"" + fixedRateString + "\"; " + ex); } - tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay))); + tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse))); } } - // Check whether we had any attribute set - Assert.isTrue(processedSchedule, errorMessage); + if (!processedSchedule) { + if (initialDelay.isNegative()) { + throw new IllegalArgumentException("One-time task only supported with specified initial delay"); + } + tasks.add(this.registrar.scheduleOneTimeTask(new OneTimeTask(runnable, delayToUse))); + } // Finally register the scheduled tasks synchronized (this.scheduledTasks) { @@ -520,13 +530,31 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) *

The default implementation creates a {@link ScheduledMethodRunnable}. * @param target the target bean instance * @param method the scheduled method to call - * @since 5.1 - * @see ScheduledMethodRunnable#ScheduledMethodRunnable(Object, Method) + * @since 6.1 */ - protected Runnable createRunnable(Object target, Method method) { + @SuppressWarnings("deprecation") + protected Runnable createRunnable(Object target, Method method, @Nullable String qualifier) { + Runnable runnable = createRunnable(target, method); + if (runnable != null) { + return runnable; + } Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled"); Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass()); - return new ScheduledMethodRunnable(target, invocableMethod); + return new ScheduledMethodRunnable(target, invocableMethod, qualifier, this.registrar::getObservationRegistry); + } + + /** + * Create a {@link Runnable} for the given bean instance, + * calling the specified scheduled method. + * @param target the target bean instance + * @param method the scheduled method to call + * @since 5.1 + * @deprecated in favor of {@link #createRunnable(Object, Method, String)} + */ + @Deprecated(since = "6.1") + @Nullable + protected Runnable createRunnable(Object target, Method method) { + return null; } private static Duration toDuration(long value, TimeUnit timeUnit) { @@ -558,6 +586,8 @@ private static boolean isP(char ch) { /** * Return all currently scheduled tasks, from {@link Scheduled} methods * as well as from programmatic {@link SchedulingConfigurer} interaction. + *

Note that this includes upcoming scheduled subscriptions for reactive + * methods but doesn't cover any currently active subscription for such methods. * @since 5.0.2 */ @Override @@ -575,21 +605,33 @@ public Set getScheduledTasks() { @Override public void postProcessBeforeDestruction(Object bean, String beanName) { + cancelScheduledTasks(bean); + this.manualCancellationOnContextClose.remove(bean); + } + + @Override + public boolean requiresDestruction(Object bean) { + synchronized (this.scheduledTasks) { + return (this.scheduledTasks.containsKey(bean) || this.reactiveSubscriptions.containsKey(bean)); + } + } + + private void cancelScheduledTasks(Object bean) { Set tasks; + List liveSubscriptions; synchronized (this.scheduledTasks) { tasks = this.scheduledTasks.remove(bean); + liveSubscriptions = this.reactiveSubscriptions.remove(bean); } if (tasks != null) { for (ScheduledTask task : tasks) { task.cancel(false); } } - } - - @Override - public boolean requiresDestruction(Object bean) { - synchronized (this.scheduledTasks) { - return this.scheduledTasks.containsKey(bean); + if (liveSubscriptions != null) { + for (Runnable subscription : liveSubscriptions) { + subscription.run(); // equivalent to cancelling the subscription + } } } @@ -603,8 +645,44 @@ public void destroy() { } } this.scheduledTasks.clear(); + Collection> allLiveSubscriptions = this.reactiveSubscriptions.values(); + for (List liveSubscriptions : allLiveSubscriptions) { + for (Runnable liveSubscription : liveSubscriptions) { + liveSubscription.run(); // equivalent to cancelling the subscription + } + } + this.reactiveSubscriptions.clear(); + this.manualCancellationOnContextClose.clear(); } + this.registrar.destroy(); + if (this.localScheduler != null) { + this.localScheduler.destroy(); + } + } + + + /** + * Reacts to {@link ContextRefreshedEvent} as well as {@link ContextClosedEvent}: + * performing {@link #finishRegistration()} and early cancelling of scheduled tasks, + * respectively. + */ + @Override + public void onApplicationEvent(ApplicationContextEvent event) { + if (event.getApplicationContext() == this.applicationContext) { + if (event instanceof ContextRefreshedEvent) { + // Running in an ApplicationContext -> register tasks this late... + // giving other ContextRefreshedEvent listeners a chance to perform + // their work at the same time (e.g. Spring Batch's job registration). + finishRegistration(); + } + else if (event instanceof ContextClosedEvent) { + for (Object bean : this.manualCancellationOnContextClose) { + cancelScheduledTasks(bean); + } + this.manualCancellationOnContextClose.clear(); + } + } } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java new file mode 100644 index 000000000000..4cd82cf647de --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java @@ -0,0 +1,336 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.annotation; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.Supplier; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Flux; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.CoroutinesUtils; +import org.springframework.core.KotlinDetector; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention; +import org.springframework.scheduling.support.ScheduledTaskObservationContext; +import org.springframework.scheduling.support.ScheduledTaskObservationConvention; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import static org.springframework.scheduling.support.ScheduledTaskObservationDocumentation.TASKS_SCHEDULED_EXECUTION; + +/** + * Helper class for @{@link ScheduledAnnotationBeanPostProcessor} to support reactive + * cases without a dependency on optional classes. + * + * @author Simon Baslé + * @author Brian Clozel + * @since 6.1 + */ +abstract class ScheduledAnnotationReactiveSupport { + + static final boolean reactorPresent = ClassUtils.isPresent( + "reactor.core.publisher.Flux", ScheduledAnnotationReactiveSupport.class.getClassLoader()); + + static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + "kotlinx.coroutines.reactor.MonoKt", ScheduledAnnotationReactiveSupport.class.getClassLoader()); + + private static final Log logger = LogFactory.getLog(ScheduledAnnotationReactiveSupport.class); + + + /** + * Checks that if the method is reactive, it can be scheduled. Methods are considered + * eligible for reactive scheduling if they either return an instance of a type that + * can be converted to {@code Publisher} or are a Kotlin suspending function. + * If the method doesn't match these criteria, this check returns {@code false}. + *

For scheduling of Kotlin suspending functions, the Coroutine-Reactor bridge + * {@code kotlinx.coroutines.reactor} must be present at runtime (in order to invoke + * suspending functions as a {@code Publisher}). Provided that is the case, this + * method returns {@code true}. Otherwise, it throws an {@code IllegalStateException}. + * @throws IllegalStateException if the method is reactive but Reactor and/or the + * Kotlin coroutines bridge are not present at runtime + */ + public static boolean isReactive(Method method) { + if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(method)) { + // Note that suspending functions declared without args have a single Continuation + // parameter in reflective inspection + Assert.isTrue(method.getParameterCount() == 1, + "Kotlin suspending functions may only be annotated with @Scheduled if declared without arguments"); + Assert.isTrue(coroutinesReactorPresent, "Kotlin suspending functions may only be annotated with " + + "@Scheduled if the Coroutine-Reactor bridge (kotlinx.coroutines.reactor) is present at runtime"); + return true; + } + ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + if (!registry.hasAdapters()) { + return false; + } + Class returnType = method.getReturnType(); + ReactiveAdapter candidateAdapter = registry.getAdapter(returnType); + if (candidateAdapter == null) { + return false; + } + Assert.isTrue(method.getParameterCount() == 0, + "Reactive methods may only be annotated with @Scheduled if declared without arguments"); + Assert.isTrue(candidateAdapter.getDescriptor().isDeferred(), + "Reactive methods may only be annotated with @Scheduled if the return type supports deferred execution"); + return true; + } + + /** + * Create a {@link Runnable} for the Scheduled infrastructure, allowing for scheduled + * subscription to the publisher produced by a reactive method. + *

Note that the reactive method is invoked once, but the resulting {@code Publisher} + * is subscribed to repeatedly, once per each invocation of the {@code Runnable}. + *

In the case of a fixed-delay configuration, the subscription inside the + * {@link Runnable} is turned into a blocking call in order to maintain fixed-delay + * semantics (i.e. the task blocks until completion of the Publisher, and the + * delay is applied until the next iteration). + */ + public static Runnable createSubscriptionRunnable(Method method, Object targetBean, Scheduled scheduled, + Supplier observationRegistrySupplier, List subscriptionTrackerRegistry) { + + boolean shouldBlock = (scheduled.fixedDelay() > 0 || StringUtils.hasText(scheduled.fixedDelayString())); + Publisher publisher = getPublisherFor(method, targetBean); + Supplier contextSupplier = + () -> new ScheduledTaskObservationContext(targetBean, method); + return new SubscribingRunnable(publisher, shouldBlock, scheduled.scheduler(), + subscriptionTrackerRegistry, observationRegistrySupplier, contextSupplier); + } + + /** + * Turn the invocation of the provided {@code Method} into a {@code Publisher}, + * either by reflectively invoking it and converting the result to a {@code Publisher} + * via {@link ReactiveAdapterRegistry} or by converting a Kotlin suspending function + * into a {@code Publisher} via {@link CoroutinesUtils}. + *

The {@link #isReactive(Method)} check is a precondition to calling this method. + * If Reactor is present at runtime, the {@code Publisher} is additionally converted + * to a {@code Flux} with a checkpoint String, allowing for better debugging. + */ + static Publisher getPublisherFor(Method method, Object bean) { + if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(method)) { + return CoroutinesUtils.invokeSuspendingFunction(method, bean, (Object[]) method.getParameters()); + } + + ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + Class returnType = method.getReturnType(); + ReactiveAdapter adapter = registry.getAdapter(returnType); + if (adapter == null) { + throw new IllegalArgumentException("Cannot convert @Scheduled reactive method return type to Publisher"); + } + if (!adapter.getDescriptor().isDeferred()) { + throw new IllegalArgumentException("Cannot convert @Scheduled reactive method return type to Publisher: " + + returnType.getSimpleName() + " is not a deferred reactive type"); + } + + Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass()); + try { + ReflectionUtils.makeAccessible(invocableMethod); + Object returnValue = invocableMethod.invoke(bean); + + Publisher publisher = adapter.toPublisher(returnValue); + // If Reactor is on the classpath, we could benefit from having a checkpoint for debuggability + if (reactorPresent) { + return Flux.from(publisher).checkpoint( + "@Scheduled '"+ method.getName() + "()' in '" + method.getDeclaringClass().getName() + "'"); + } + else { + return publisher; + } + } + catch (InvocationTargetException ex) { + throw new IllegalArgumentException( + "Cannot obtain a Publisher-convertible value from the @Scheduled reactive method", + ex.getTargetException()); + } + catch (IllegalAccessException ex) { + throw new IllegalArgumentException( + "Cannot obtain a Publisher-convertible value from the @Scheduled reactive method", ex); + } + } + + + /** + * Utility implementation of {@code Runnable} that subscribes to a {@code Publisher} + * or subscribes-then-blocks if {@code shouldBlock} is set to {@code true}. + */ + static final class SubscribingRunnable implements SchedulingAwareRunnable { + + private static final ScheduledTaskObservationConvention DEFAULT_CONVENTION = + new DefaultScheduledTaskObservationConvention(); + + private final Publisher publisher; + + final boolean shouldBlock; + + @Nullable + private final String qualifier; + + private final List subscriptionTrackerRegistry; + + final Supplier observationRegistrySupplier; + + final Supplier contextSupplier; + + SubscribingRunnable(Publisher publisher, boolean shouldBlock, + @Nullable String qualifier, List subscriptionTrackerRegistry, + Supplier observationRegistrySupplier, + Supplier contextSupplier) { + + this.publisher = publisher; + this.shouldBlock = shouldBlock; + this.qualifier = qualifier; + this.subscriptionTrackerRegistry = subscriptionTrackerRegistry; + this.observationRegistrySupplier = observationRegistrySupplier; + this.contextSupplier = contextSupplier; + } + + @Override + @Nullable + public String getQualifier() { + return this.qualifier; + } + + @Override + public void run() { + Observation observation = TASKS_SCHEDULED_EXECUTION.observation(null, DEFAULT_CONVENTION, + this.contextSupplier, this.observationRegistrySupplier.get()); + if (this.shouldBlock) { + CountDownLatch latch = new CountDownLatch(1); + TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, observation, latch); + subscribe(subscriber, observation); + try { + latch.await(); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + else { + TrackingSubscriber subscriber = new TrackingSubscriber(this.subscriptionTrackerRegistry, observation); + subscribe(subscriber, observation); + } + } + + private void subscribe(TrackingSubscriber subscriber, Observation observation) { + this.subscriptionTrackerRegistry.add(subscriber); + if (reactorPresent) { + observation.start(); + Flux.from(this.publisher) + .contextWrite(context -> context.put(ObservationThreadLocalAccessor.KEY, observation)) + .subscribe(subscriber); + } + else { + this.publisher.subscribe(subscriber); + } + } + } + + + /** + * A {@code Subscriber} which keeps track of its {@code Subscription} and exposes the + * capacity to cancel the subscription as a {@code Runnable}. Can optionally support + * blocking if a {@code CountDownLatch} is supplied during construction. + */ + private static final class TrackingSubscriber implements Subscriber, Runnable { + + private final List subscriptionTrackerRegistry; + + private final Observation observation; + + @Nullable + private final CountDownLatch blockingLatch; + + // Implementation note: since this is created last-minute when subscribing, + // there shouldn't be a way to cancel the tracker externally from the + // ScheduledAnnotationBeanProcessor before the #setSubscription(Subscription) + // method is called. + @Nullable + private Subscription subscription; + + TrackingSubscriber(List subscriptionTrackerRegistry, Observation observation) { + this(subscriptionTrackerRegistry, observation, null); + } + + TrackingSubscriber(List subscriptionTrackerRegistry, Observation observation, @Nullable CountDownLatch latch) { + this.subscriptionTrackerRegistry = subscriptionTrackerRegistry; + this.observation = observation; + this.blockingLatch = latch; + } + + @Override + public void run() { + if (this.subscription != null) { + this.subscription.cancel(); + this.observation.stop(); + } + if (this.blockingLatch != null) { + this.blockingLatch.countDown(); + } + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + subscription.request(Integer.MAX_VALUE); + } + + @Override + public void onNext(Object obj) { + // no-op + } + + @Override + public void onError(Throwable ex) { + this.subscriptionTrackerRegistry.remove(this); + logger.warn("Unexpected error occurred in scheduled reactive task", ex); + this.observation.error(ex); + this.observation.stop(); + if (this.blockingLatch != null) { + this.blockingLatch.countDown(); + } + } + + @Override + public void onComplete() { + this.subscriptionTrackerRegistry.remove(this); + if (this.observation.getContext() instanceof ScheduledTaskObservationContext context) { + context.setComplete(true); + } + this.observation.stop(); + if (this.blockingLatch != null) { + this.blockingLatch.countDown(); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java index f0fd31a8abd1..d9bd5ee06b1b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,10 @@ @SuppressWarnings("deprecation") public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { + private static final Executor STUB_EXECUTOR = (task -> { + throw new IllegalStateException("Executor not configured"); + }); + @Nullable private static Class managedExecutorServiceClass; @@ -80,9 +84,10 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche } } - private Executor concurrentExecutor; - private TaskExecutorAdapter adaptedExecutor; + private Executor concurrentExecutor = STUB_EXECUTOR; + + private TaskExecutorAdapter adaptedExecutor = new TaskExecutorAdapter(STUB_EXECUTOR); @Nullable private TaskDecorator taskDecorator; @@ -91,7 +96,10 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche /** * Create a new ConcurrentTaskExecutor, using a single thread executor as default. * @see java.util.concurrent.Executors#newSingleThreadExecutor() + * @deprecated in favor of {@link #ConcurrentTaskExecutor(Executor)} with an + * externally provided Executor */ + @Deprecated(since = "6.1") public ConcurrentTaskExecutor() { this.concurrentExecutor = Executors.newSingleThreadExecutor(); this.adaptedExecutor = new TaskExecutorAdapter(this.concurrentExecutor); @@ -104,8 +112,9 @@ public ConcurrentTaskExecutor() { * @param executor the {@link java.util.concurrent.Executor} to delegate to */ public ConcurrentTaskExecutor(@Nullable Executor executor) { - this.concurrentExecutor = (executor != null ? executor : Executors.newSingleThreadExecutor()); - this.adaptedExecutor = getAdaptedExecutor(this.concurrentExecutor); + if (executor != null) { + setConcurrentExecutor(executor); + } } @@ -114,8 +123,8 @@ public ConcurrentTaskExecutor(@Nullable Executor executor) { *

Autodetects a JSR-236 {@link jakarta.enterprise.concurrent.ManagedExecutorService} * in order to expose {@link jakarta.enterprise.concurrent.ManagedTask} adapters for it. */ - public final void setConcurrentExecutor(@Nullable Executor executor) { - this.concurrentExecutor = (executor != null ? executor : Executors.newSingleThreadExecutor()); + public final void setConcurrentExecutor(Executor executor) { + this.concurrentExecutor = executor; this.adaptedExecutor = getAdaptedExecutor(this.concurrentExecutor); } @@ -134,11 +143,6 @@ public final Executor getConcurrentExecutor() { * execution callback (which may be a wrapper around the user-supplied task). *

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

NOTE: Exception handling in {@code TaskDecorator} implementations - * is limited to plain {@code Runnable} execution via {@code execute} calls. - * In case of {@code #submit} calls, the exposed {@code Runnable} will be a - * {@code FutureTask} which does not propagate any exceptions; you might - * have to cast it and call {@code Future#get} to evaluate exceptions. * @since 4.3 */ public final void setTaskDecorator(TaskDecorator taskDecorator) { @@ -179,11 +183,10 @@ public ListenableFuture submitListenable(Callable task) { } - private TaskExecutorAdapter getAdaptedExecutor(Executor concurrentExecutor) { - if (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(concurrentExecutor)) { - return new ManagedTaskExecutorAdapter(concurrentExecutor); - } - TaskExecutorAdapter adapter = new TaskExecutorAdapter(concurrentExecutor); + private TaskExecutorAdapter getAdaptedExecutor(Executor originalExecutor) { + TaskExecutorAdapter adapter = + (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(originalExecutor) ? + new ManagedTaskExecutorAdapter(originalExecutor) : new TaskExecutorAdapter(originalExecutor)); if (this.taskDecorator != null) { adapter.setTaskDecorator(this.taskDecorator); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java index 9b3e65b33ff7..0048321d2aac 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,6 +89,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T } + @Nullable private ScheduledExecutorService scheduledExecutor; private boolean enterpriseConcurrentScheduler = false; @@ -103,10 +104,14 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T * Create a new ConcurrentTaskScheduler, * using a single thread executor as default. * @see java.util.concurrent.Executors#newSingleThreadScheduledExecutor() + * @deprecated in favor of {@link #ConcurrentTaskScheduler(ScheduledExecutorService)} + * with an externally provided Executor */ + @Deprecated(since = "6.1") public ConcurrentTaskScheduler() { super(); - initScheduledExecutor(null); + this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); + this.enterpriseConcurrentScheduler = false; } /** @@ -119,9 +124,11 @@ public ConcurrentTaskScheduler() { * to delegate to for {@link org.springframework.scheduling.SchedulingTaskExecutor} * as well as {@link TaskScheduler} invocations */ - public ConcurrentTaskScheduler(ScheduledExecutorService scheduledExecutor) { + public ConcurrentTaskScheduler(@Nullable ScheduledExecutorService scheduledExecutor) { super(scheduledExecutor); - initScheduledExecutor(scheduledExecutor); + if (scheduledExecutor != null) { + initScheduledExecutor(scheduledExecutor); + } } /** @@ -141,16 +148,10 @@ public ConcurrentTaskScheduler(Executor concurrentExecutor, ScheduledExecutorSer } - private void initScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) { - if (scheduledExecutor != null) { - this.scheduledExecutor = scheduledExecutor; - this.enterpriseConcurrentScheduler = (managedScheduledExecutorServiceClass != null && - managedScheduledExecutorServiceClass.isInstance(scheduledExecutor)); - } - else { - this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); - this.enterpriseConcurrentScheduler = false; - } + private void initScheduledExecutor(ScheduledExecutorService scheduledExecutor) { + this.scheduledExecutor = scheduledExecutor; + this.enterpriseConcurrentScheduler = (managedScheduledExecutorServiceClass != null && + managedScheduledExecutorServiceClass.isInstance(scheduledExecutor)); } /** @@ -164,10 +165,17 @@ private void initScheduledExecutor(@Nullable ScheduledExecutorService scheduledE * as well, pass the same executor reference to {@link #setConcurrentExecutor}. * @see #setConcurrentExecutor */ - public void setScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) { + public void setScheduledExecutor(ScheduledExecutorService scheduledExecutor) { initScheduledExecutor(scheduledExecutor); } + private ScheduledExecutorService getScheduledExecutor() { + if (this.scheduledExecutor == null) { + throw new IllegalStateException("No ScheduledExecutor is configured"); + } + return this.scheduledExecutor; + } + /** * Provide an {@link ErrorHandler} strategy. */ @@ -183,6 +191,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -195,6 +204,7 @@ public Clock getClock() { @Override @Nullable public ScheduledFuture schedule(Runnable task, Trigger trigger) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); try { if (this.enterpriseConcurrentScheduler) { return new EnterpriseConcurrentTriggerScheduler().schedule(decorateTask(task, true), trigger); @@ -202,68 +212,73 @@ public ScheduledFuture schedule(Runnable task, Trigger trigger) { else { ErrorHandler errorHandler = (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true)); - return new ReschedulingRunnable(task, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule(); + return new ReschedulingRunnable(task, trigger, this.clock, scheduleExecutorToUse, errorHandler).schedule(); } } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture schedule(Runnable task, Instant startTime) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); Duration delay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.schedule(decorateTask(task, false), NANO.convert(delay), NANO); + return scheduleExecutorToUse.schedule(decorateTask(task, false), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), + return scheduleExecutorToUse.scheduleAtFixedRate(decorateTask(task, true), NANO.convert(initialDelay), NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); try { - return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), + return scheduleExecutorToUse.scheduleAtFixedRate(decorateTask(task, true), 0, NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), + return scheduleExecutorToUse.scheduleWithFixedDelay(decorateTask(task, true), NANO.convert(initialDelay), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); try { - return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), + return scheduleExecutorToUse.scheduleWithFixedDelay(decorateTask(task, true), 0, NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @@ -283,7 +298,7 @@ private Runnable decorateTask(Runnable task, boolean isRepeatingTask) { private class EnterpriseConcurrentTriggerScheduler { public ScheduledFuture schedule(Runnable task, Trigger trigger) { - ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService) scheduledExecutor; + ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService) getScheduledExecutor(); return executor.schedule(task, new TriggerAdapter(trigger)); } @@ -319,16 +334,19 @@ public LastExecutionAdapter(@Nullable LastExecution le) { } @Override + @Nullable public Instant lastScheduledExecution() { return (this.le != null ? toInstant(this.le.getScheduledStart()) : null); } @Override + @Nullable public Instant lastActualExecution() { return (this.le != null ? toInstant(this.le.getRunStart()) : null); } @Override + @Nullable public Instant lastCompletion() { return (this.le != null ? toInstant(this.le.getRunEnd()) : null); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskExecutor.java index bf40a15f3dd2..b3808eb1a185 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.jndi.JndiLocatorDelegate; import org.springframework.jndi.JndiTemplate; -import org.springframework.lang.Nullable; /** * JNDI-based variant of {@link ConcurrentTaskExecutor}, performing a default lookup for @@ -43,10 +42,15 @@ public class DefaultManagedTaskExecutor extends ConcurrentTaskExecutor implement private final JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); - @Nullable private String jndiName = "java:comp/DefaultManagedExecutorService"; + public DefaultManagedTaskExecutor() { + // Executor initialization happens in afterPropertiesSet + super(null); + } + + /** * Set the JNDI template to use for JNDI lookups. * @see org.springframework.jndi.JndiAccessor#setJndiTemplate @@ -87,9 +91,7 @@ public void setJndiName(String jndiName) { @Override public void afterPropertiesSet() throws NamingException { - if (this.jndiName != null) { - setConcurrentExecutor(this.jndiLocator.lookup(this.jndiName, Executor.class)); - } + setConcurrentExecutor(this.jndiLocator.lookup(this.jndiName, Executor.class)); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java index b1845b563c17..b41315f86d55 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,11 +24,12 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.jndi.JndiLocatorDelegate; import org.springframework.jndi.JndiTemplate; -import org.springframework.lang.Nullable; /** * JNDI-based variant of {@link ConcurrentTaskScheduler}, performing a default lookup for * JSR-236's "java:comp/DefaultManagedScheduledExecutorService" in a Jakarta EE environment. + * Expected to be exposed as a bean, in particular as the default lookup happens in the + * standard {@link InitializingBean#afterPropertiesSet()} callback. * *

Note: This class is not strictly JSR-236 based; it can work with any regular * {@link java.util.concurrent.ScheduledExecutorService} that can be found in JNDI. @@ -43,10 +44,15 @@ public class DefaultManagedTaskScheduler extends ConcurrentTaskScheduler impleme private final JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); - @Nullable private String jndiName = "java:comp/DefaultManagedScheduledExecutorService"; + public DefaultManagedTaskScheduler() { + // Executor initialization happens in afterPropertiesSet + super(null); + } + + /** * Set the JNDI template to use for JNDI lookups. * @see org.springframework.jndi.JndiAccessor#setJndiTemplate @@ -87,11 +93,9 @@ public void setJndiName(String jndiName) { @Override public void afterPropertiesSet() throws NamingException { - if (this.jndiName != null) { - ScheduledExecutorService executor = this.jndiLocator.lookup(this.jndiName, ScheduledExecutorService.class); - setConcurrentExecutor(executor); - setScheduledExecutor(executor); - } + ScheduledExecutorService executor = this.jndiLocator.lookup(this.jndiName, ScheduledExecutorService.class); + setConcurrentExecutor(executor); + setScheduledExecutor(executor); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index ae4d3a1e34ab..5f20eb75aff7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,11 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.lang.Nullable; /** @@ -50,7 +55,8 @@ */ @SuppressWarnings("serial") public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory - implements BeanNameAware, InitializingBean, DisposableBean { + implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, + SmartLifecycle, ApplicationListener { protected final Log logger = LogFactory.getLog(getClass()); @@ -60,20 +66,32 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy(); + private boolean acceptTasksAfterContextClose = false; + private boolean waitForTasksToCompleteOnShutdown = false; private long awaitTerminationMillis = 0; + private int phase = DEFAULT_PHASE; + @Nullable private String beanName; + @Nullable + private ApplicationContext applicationContext; + @Nullable private ExecutorService executor; + @Nullable + private ExecutorLifecycleDelegate lifecycleDelegate; + + private volatile boolean lateShutdown; + /** * Set the ThreadFactory to use for the ExecutorService's thread pool. - * Default is the underlying ExecutorService's default thread factory. + * The default is the underlying ExecutorService's default thread factory. *

In a Jakarta EE or other managed environment with JSR-236 support, * consider specifying a JNDI-located ManagedThreadFactory: by default, * to be found at "java:comp/DefaultManagedThreadFactory". @@ -97,7 +115,7 @@ public void setThreadNamePrefix(@Nullable String threadNamePrefix) { /** * Set the RejectedExecutionHandler to use for the ExecutorService. - * Default is the ExecutorService's default abort policy. + * The default is the ExecutorService's default abort policy. * @see java.util.concurrent.ThreadPoolExecutor.AbortPolicy */ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejectedExecutionHandler) { @@ -105,12 +123,47 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec (rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy()); } + /** + * Set whether to accept further tasks after the application context close phase + * has begun. + *

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

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

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

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

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

Note that Spring's container shutdown continues while ongoing tasks * are being completed. If you want this executor to block and wait for the * termination of tasks before the rest of the container continues to shut @@ -119,6 +172,8 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec * property instead of or in addition to this property. * @see java.util.concurrent.ExecutorService#shutdown() * @see java.util.concurrent.ExecutorService#shutdownNow() + * @see #shutdown() + * @see #setAwaitTerminationSeconds */ public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; @@ -161,11 +216,36 @@ public void setAwaitTerminationMillis(long awaitTerminationMillis) { this.awaitTerminationMillis = awaitTerminationMillis; } + /** + * Specify the lifecycle phase for pausing and resuming this executor. + * The default is {@link #DEFAULT_PHASE}. + * @since 6.1 + * @see SmartLifecycle#getPhase() + */ + public void setPhase(int phase) { + this.phase = phase; + } + + /** + * Return the lifecycle phase for pausing and resuming this executor. + * @since 6.1 + * @see #setPhase + */ + @Override + public int getPhase() { + return this.phase; + } + @Override public void setBeanName(String name) { this.beanName = name; } + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + /** * Calls {@code initialize()} after the container applied all property values. @@ -187,6 +267,7 @@ public void initialize() { setThreadNamePrefix(this.beanName + "-"); } this.executor = initializeExecutor(this.threadFactory, this.rejectedExecutionHandler); + this.lifecycleDelegate = new ExecutorLifecycleDelegate(this.executor); } /** @@ -210,9 +291,32 @@ public void destroy() { shutdown(); } + /** + * Initiate a shutdown on the underlying ExecutorService, + * rejecting further task submissions. + *

The executor will not accept further tasks and will prevent further + * scheduling of periodic tasks, letting existing tasks complete still. + * This step is non-blocking and can be applied as an early shutdown signal + * before following up with a full {@link #shutdown()} call later on. + *

Automatically called for early shutdown signals on + * {@link #onApplicationEvent(ContextClosedEvent) context close}. + * Can be manually called as well, in particular outside a container. + * @since 6.1 + * @see #shutdown() + * @see java.util.concurrent.ExecutorService#shutdown() + */ + public void initiateShutdown() { + if (this.executor != null) { + this.executor.shutdown(); + } + } + /** * Perform a full shutdown on the underlying ExecutorService, * according to the corresponding configuration settings. + *

This step potentially blocks for the configured termination period, + * waiting for remaining tasks to complete. For an early shutdown signal + * to not accept further tasks, call {@link #initiateShutdown()} first. * @see #setWaitForTasksToCompleteOnShutdown * @see #setAwaitTerminationMillis * @see java.util.concurrent.ExecutorService#shutdown() @@ -237,7 +341,7 @@ public void shutdown() { } /** - * Cancel the given remaining task which never commended execution, + * Cancel the given remaining task which never commenced execution, * as returned from {@link ExecutorService#shutdownNow()}. * @param task the task to cancel (typically a {@link RunnableFuture}) * @since 5.0.5 @@ -274,4 +378,121 @@ private void awaitTerminationIfNecessary(ExecutorService executor) { } } + + /** + * Resume this executor if paused before (otherwise a no-op). + * @since 6.1 + */ + @Override + public void start() { + if (this.lifecycleDelegate != null) { + this.lifecycleDelegate.start(); + } + } + + /** + * Pause this executor, not waiting for tasks to complete. + * @since 6.1 + */ + @Override + public void stop() { + if (this.lifecycleDelegate != null && !this.lateShutdown) { + this.lifecycleDelegate.stop(); + } + } + + /** + * Pause this executor, triggering the given callback + * once all currently executing tasks have completed. + * @since 6.1 + */ + @Override + public void stop(Runnable callback) { + if (this.lifecycleDelegate != null && !this.lateShutdown) { + this.lifecycleDelegate.stop(callback); + } + else { + callback.run(); + } + } + + /** + * Check whether this executor is not paused and has not been shut down either. + * @since 6.1 + * @see #start() + * @see #stop() + */ + @Override + public boolean isRunning() { + return (this.lifecycleDelegate != null && this.lifecycleDelegate.isRunning()); + } + + /** + * A before-execute callback for framework subclasses to delegate to + * (for start/stop handling), and possibly also for custom subclasses + * to extend (making sure to call this implementation as well). + * @param thread the thread to run the task + * @param task the task to be executed + * @since 6.1 + * @see ThreadPoolExecutor#beforeExecute(Thread, Runnable) + */ + protected void beforeExecute(Thread thread, Runnable task) { + if (this.lifecycleDelegate != null) { + this.lifecycleDelegate.beforeExecute(thread); + } + } + + /** + * An after-execute callback for framework subclasses to delegate to + * (for start/stop handling), and possibly also for custom subclasses + * to extend (making sure to call this implementation as well). + * @param task the task that has been executed + * @param ex the exception thrown during execution, if any + * @since 6.1 + * @see ThreadPoolExecutor#afterExecute(Runnable, Throwable) + */ + protected void afterExecute(Runnable task, @Nullable Throwable ex) { + if (this.lifecycleDelegate != null) { + this.lifecycleDelegate.afterExecute(); + } + } + + /** + * {@link ContextClosedEvent} handler for initiating an early shutdown. + * @since 6.1 + * @see #initiateShutdown() + */ + @Override + public void onApplicationEvent(ContextClosedEvent event) { + if (event.getApplicationContext() == this.applicationContext) { + if (this.acceptTasksAfterContextClose || this.waitForTasksToCompleteOnShutdown) { + // Late shutdown without early stop lifecycle. + this.lateShutdown = true; + } + else { + if (this.lifecycleDelegate != null) { + this.lifecycleDelegate.markShutdown(); + } + initiateEarlyShutdown(); + } + } + } + + /** + * Early shutdown signal: do not trigger further tasks, let existing tasks complete + * before hitting the actual destruction step in the {@link #shutdown()} method. + * This goes along with a {@link #stop(Runnable) coordinated lifecycle stop phase}. + *

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

The default implementation calls {@link #initiateShutdown()}. + * @since 6.1.4 + * @see #initiateShutdown() + */ + protected void initiateEarlyShutdown() { + initiateShutdown(); + } + } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java new file mode 100644 index 000000000000..e8e705fc2179 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.concurrent; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.springframework.context.SmartLifecycle; +import org.springframework.lang.Nullable; + +/** + * An internal delegate for common {@link ExecutorService} lifecycle management + * with pause/resume support. + * + * @author Juergen Hoeller + * @since 6.1 + * @see ExecutorConfigurationSupport + * @see SimpleAsyncTaskScheduler + */ +final class ExecutorLifecycleDelegate implements SmartLifecycle { + + private final ExecutorService executor; + + private final Lock pauseLock = new ReentrantLock(); + + private final Condition unpaused = this.pauseLock.newCondition(); + + private volatile boolean paused; + + private volatile boolean shutdown; + + private int executingTaskCount = 0; + + @Nullable + private Runnable stopCallback; + + + public ExecutorLifecycleDelegate(ExecutorService executor) { + this.executor = executor; + } + + + @Override + public void start() { + this.pauseLock.lock(); + try { + this.paused = false; + this.unpaused.signalAll(); + } + finally { + this.pauseLock.unlock(); + } + } + + @Override + public void stop() { + this.pauseLock.lock(); + try { + this.paused = true; + this.stopCallback = null; + } + finally { + this.pauseLock.unlock(); + } + } + + @Override + public void stop(Runnable callback) { + this.pauseLock.lock(); + try { + this.paused = true; + if (this.executingTaskCount == 0) { + this.stopCallback = null; + callback.run(); + } + else { + this.stopCallback = callback; + } + } + finally { + this.pauseLock.unlock(); + } + } + + @Override + public boolean isRunning() { + return (!this.paused && !this.executor.isTerminated()); + } + + void markShutdown() { + this.shutdown = true; + } + + void beforeExecute(Thread thread) { + this.pauseLock.lock(); + try { + while (this.paused && !this.shutdown && !this.executor.isShutdown()) { + this.unpaused.await(); + } + } + catch (InterruptedException ex) { + thread.interrupt(); + } + finally { + this.executingTaskCount++; + this.pauseLock.unlock(); + } + } + + void afterExecute() { + this.pauseLock.lock(); + try { + this.executingTaskCount--; + if (this.executingTaskCount == 0) { + Runnable callback = this.stopCallback; + if (callback != null) { + callback.run(); + this.stopCallback = null; + } + } + } + finally { + this.pauseLock.unlock(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java index 9f01593d335b..c41101b49953 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java @@ -187,7 +187,16 @@ protected ExecutorService initializeExecutor( protected ScheduledExecutorService createExecutor( int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { - return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler); + return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler) { + @Override + protected void beforeExecute(Thread thread, Runnable task) { + ScheduledExecutorFactoryBean.this.beforeExecute(thread, task); + } + @Override + protected void afterExecute(Runnable task, Throwable ex) { + ScheduledExecutorFactoryBean.this.afterExecute(task, ex); + } + }; } /** diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java new file mode 100644 index 000000000000..027d1f6f4f47 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java @@ -0,0 +1,324 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.concurrent; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; + +/** + * A simple implementation of Spring's {@link TaskScheduler} interface, using + * a single scheduler thread and executing every scheduled task in an individual + * separate thread. This is an attractive choice with virtual threads on JDK 21, + * expecting common usage with {@link #setVirtualThreads setVirtualThreads(true)}. + * + *

NOTE: Scheduling with a fixed delay enforces execution on the single + * scheduler thread, in order to provide traditional fixed-delay semantics! + * Prefer the use of fixed rates or cron triggers instead which are a better fit + * with this thread-per-task scheduler variant. + * + *

Supports a graceful shutdown through {@link #setTaskTerminationTimeout}, + * at the expense of task tracking overhead per execution thread at runtime. + * Supports limiting concurrent threads through {@link #setConcurrencyLimit}. + * By default, the number of concurrent task executions is unlimited. + * This allows for dynamic concurrency of scheduled task executions, in contrast + * to {@link ThreadPoolTaskScheduler} which requires a fixed pool size. + * + *

NOTE: This implementation does not reuse threads! Consider a + * thread-pooling TaskScheduler implementation instead, in particular for + * scheduling a large number of short-lived tasks. Alternatively, on JDK 21, + * consider setting {@link #setVirtualThreads} to {@code true}. + * + *

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

NOTE: This scheduler variant does not track the actual completion of tasks + * but rather just the hand-off to an execution thread. As a consequence, + * a {@link ScheduledFuture} handle (e.g. from {@link #schedule(Runnable, Instant)}) + * represents that hand-off rather than the actual completion of the provided task + * (or series of repeated tasks). + * + *

As an alternative to the built-in thread-per-task capability, this scheduler + * can also be configured with a separate target executor for scheduled task + * execution through {@link #setTargetTaskExecutor}: e.g. pointing to a shared + * {@link ThreadPoolTaskExecutor} bean. This is still rather different from a + * {@link ThreadPoolTaskScheduler} setup since it always uses a single scheduler + * thread while dynamically dispatching to the target thread pool which may have + * a dynamic core/max pool size range, participating in a shared concurrency limit. + * + * @author Juergen Hoeller + * @since 6.1 + * @see #setVirtualThreads + * @see #setTaskTerminationTimeout + * @see #setConcurrencyLimit + * @see SimpleAsyncTaskExecutor + * @see ThreadPoolTaskScheduler + */ +@SuppressWarnings("serial") +public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements TaskScheduler, + ApplicationContextAware, SmartLifecycle, ApplicationListener { + + private static final TimeUnit NANO = TimeUnit.NANOSECONDS; + + + private final ScheduledExecutorService scheduledExecutor = createScheduledExecutor(); + + private final ExecutorLifecycleDelegate lifecycleDelegate = new ExecutorLifecycleDelegate(this.scheduledExecutor); + + private Clock clock = Clock.systemDefaultZone(); + + private int phase = DEFAULT_PHASE; + + @Nullable + private Executor targetTaskExecutor; + + @Nullable + private ApplicationContext applicationContext; + + + /** + * Set the clock to use for scheduling purposes. + *

The default clock is the system clock for the default time zone. + * @see Clock#systemDefaultZone() + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); + this.clock = clock; + } + + @Override + public Clock getClock() { + return this.clock; + } + + /** + * Specify the lifecycle phase for pausing and resuming this executor. + * The default is {@link #DEFAULT_PHASE}. + * @see SmartLifecycle#getPhase() + */ + public void setPhase(int phase) { + this.phase = phase; + } + + /** + * Return the lifecycle phase for pausing and resuming this executor. + * @see #setPhase + */ + @Override + public int getPhase() { + return this.phase; + } + + /** + * Specify a custom target {@link Executor} to delegate to for + * the individual execution of scheduled tasks. This can for example + * be set to a separate thread pool for executing scheduled tasks, + * whereas this scheduler keeps using its single scheduler thread. + *

If not set, the regular {@link SimpleAsyncTaskExecutor} + * arrangements kicks in with a new thread per task. + */ + public void setTargetTaskExecutor(Executor targetTaskExecutor) { + this.targetTaskExecutor = (targetTaskExecutor == this ? null : targetTaskExecutor); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + private ScheduledExecutorService createScheduledExecutor() { + return new ScheduledThreadPoolExecutor(1, this::newThread) { + @Override + protected void beforeExecute(Thread thread, Runnable task) { + lifecycleDelegate.beforeExecute(thread); + } + @Override + protected void afterExecute(Runnable task, Throwable ex) { + lifecycleDelegate.afterExecute(); + } + }; + } + + @Override + protected void doExecute(Runnable task) { + if (this.targetTaskExecutor != null) { + this.targetTaskExecutor.execute(task); + } + else { + super.doExecute(task); + } + } + + private Runnable taskOnSchedulerThread(Runnable task) { + return new DelegatingErrorHandlingRunnable(task, TaskUtils.getDefaultErrorHandler(true)); + } + + private Runnable scheduledTask(Runnable task) { + return () -> execute(new DelegatingErrorHandlingRunnable(task, this::shutdownAwareErrorHandler)); + } + + private void shutdownAwareErrorHandler(Throwable ex) { + if (this.scheduledExecutor.isShutdown()) { + LogFactory.getLog(getClass()).debug("Ignoring scheduled task exception after shutdown", ex); + } + else { + TaskUtils.getDefaultErrorHandler(true).handleError(ex); + } + } + + + @Override + @Nullable + public ScheduledFuture schedule(Runnable task, Trigger trigger) { + try { + Runnable delegate = scheduledTask(task); + ErrorHandler errorHandler = TaskUtils.getDefaultErrorHandler(true); + return new ReschedulingRunnable( + delegate, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule(); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException(this.scheduledExecutor, task, ex); + } + } + + @Override + public ScheduledFuture schedule(Runnable task, Instant startTime) { + Duration delay = Duration.between(this.clock.instant(), startTime); + try { + return this.scheduledExecutor.schedule(scheduledTask(task), NANO.convert(delay), NANO); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException(this.scheduledExecutor, task, ex); + } + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + Duration initialDelay = Duration.between(this.clock.instant(), startTime); + try { + return this.scheduledExecutor.scheduleAtFixedRate(scheduledTask(task), + NANO.convert(initialDelay), NANO.convert(period), NANO); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException(this.scheduledExecutor, task, ex); + } + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + try { + return this.scheduledExecutor.scheduleAtFixedRate(scheduledTask(task), + 0, NANO.convert(period), NANO); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException(this.scheduledExecutor, task, ex); + } + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + Duration initialDelay = Duration.between(this.clock.instant(), startTime); + try { + // Blocking task on scheduler thread for fixed delay semantics + return this.scheduledExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task), + NANO.convert(initialDelay), NANO.convert(delay), NANO); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException(this.scheduledExecutor, task, ex); + } + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + try { + // Blocking task on scheduler thread for fixed delay semantics + return this.scheduledExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task), + 0, NANO.convert(delay), NANO); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException(this.scheduledExecutor, task, ex); + } + } + + + @Override + public void start() { + this.lifecycleDelegate.start(); + } + + @Override + public void stop() { + this.lifecycleDelegate.stop(); + } + + @Override + public void stop(Runnable callback) { + this.lifecycleDelegate.stop(callback); + } + + @Override + public boolean isRunning() { + return this.lifecycleDelegate.isRunning(); + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + if (event.getApplicationContext() == this.applicationContext) { + this.scheduledExecutor.shutdown(); + } + } + + @Override + public void close() { + for (Runnable remainingTask : this.scheduledExecutor.shutdownNow()) { + if (remainingTask instanceof Future future) { + future.cancel(true); + } + } + super.close(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java index 3a853297b397..7680a2634f71 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,11 +71,13 @@ public class ThreadPoolExecutorFactoryBean extends ExecutorConfigurationSupport private int keepAliveSeconds = 60; + private int queueCapacity = Integer.MAX_VALUE; + private boolean allowCoreThreadTimeOut = false; private boolean prestartAllCoreThreads = false; - private int queueCapacity = Integer.MAX_VALUE; + private boolean strictEarlyShutdown = false; private boolean exposeUnconfigurableExecutor = false; @@ -107,6 +109,18 @@ public void setKeepAliveSeconds(int keepAliveSeconds) { this.keepAliveSeconds = keepAliveSeconds; } + /** + * Set the capacity for the ThreadPoolExecutor's BlockingQueue. + * Default is {@code Integer.MAX_VALUE}. + *

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

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

Default is "false". + * See {@link ThreadPoolTaskExecutor#setStrictEarlyShutdown} for details. + * @since 6.1.4 + * @see #initiateShutdown() */ - public void setQueueCapacity(int queueCapacity) { - this.queueCapacity = queueCapacity; + public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) { + this.strictEarlyShutdown = defaultEarlyShutdown; } /** @@ -158,7 +172,7 @@ protected ExecutorService initializeExecutor( ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { BlockingQueue queue = createQueue(this.queueCapacity); - ThreadPoolExecutor executor = createExecutor(this.corePoolSize, this.maxPoolSize, + ThreadPoolExecutor executor = createExecutor(this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, queue, threadFactory, rejectedExecutionHandler); if (this.allowCoreThreadTimeOut) { executor.allowCoreThreadTimeOut(true); @@ -192,7 +206,16 @@ protected ThreadPoolExecutor createExecutor( ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { return new ThreadPoolExecutor(corePoolSize, maxPoolSize, - keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler); + keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler) { + @Override + protected void beforeExecute(Thread thread, Runnable task) { + ThreadPoolExecutorFactoryBean.this.beforeExecute(thread, task); + } + @Override + protected void afterExecute(Runnable task, Throwable ex) { + ThreadPoolExecutorFactoryBean.this.afterExecute(task, ex); + } + }; } /** @@ -213,6 +236,13 @@ protected BlockingQueue createQueue(int queueCapacity) { } } + @Override + protected void initiateEarlyShutdown() { + if (this.strictEarlyShutdown) { + super.initiateEarlyShutdown(); + } + } + @Override @Nullable diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java index 144b2962ecfc..70783b24b9ed 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,6 +98,8 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport private boolean prestartAllCoreThreads = false; + private boolean strictEarlyShutdown = false; + @Nullable private TaskDecorator taskDecorator; @@ -212,7 +214,7 @@ public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { /** * Specify whether to start all core threads, causing them to idly wait for work. - *

Default is "false". + *

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

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

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

Switch this to "true" for a strict early shutdown signal analogous to + * the 6.1-established default behavior of {@link ThreadPoolTaskScheduler}. + * Note that the related flags {@link #setAcceptTasksAfterContextClose} and + * {@link #setWaitForTasksToCompleteOnShutdown} will override this setting, + * leading to a late shutdown without a coordinated lifecycle stop phase. + * @since 6.1.4 + * @see #initiateShutdown() + */ + public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) { + this.strictEarlyShutdown = defaultEarlyShutdown; + } + /** * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} * about to be executed. @@ -254,27 +280,29 @@ protected ExecutorService initializeExecutor( BlockingQueue queue = createQueue(this.queueCapacity); - ThreadPoolExecutor executor; - if (this.taskDecorator != null) { - executor = new ThreadPoolExecutor( + ThreadPoolExecutor executor = new ThreadPoolExecutor( this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler) { - @Override - public void execute(Runnable command) { - Runnable decorated = taskDecorator.decorate(command); + @Override + public void execute(Runnable command) { + Runnable decorated = command; + if (taskDecorator != null) { + decorated = taskDecorator.decorate(command); if (decorated != command) { decoratedTaskMap.put(decorated, command); } - super.execute(decorated); } - }; - } - else { - executor = new ThreadPoolExecutor( - this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, - queue, threadFactory, rejectedExecutionHandler); - - } + super.execute(decorated); + } + @Override + protected void beforeExecute(Thread thread, Runnable task) { + ThreadPoolTaskExecutor.this.beforeExecute(thread, task); + } + @Override + protected void afterExecute(Runnable task, Throwable ex) { + ThreadPoolTaskExecutor.this.afterExecute(task, ex); + } + }; if (this.allowCoreThreadTimeOut) { executor.allowCoreThreadTimeOut(true); @@ -290,7 +318,7 @@ public void execute(Runnable command) { /** * Create the BlockingQueue to use for the ThreadPoolExecutor. *

A LinkedBlockingQueue instance will be created for a positive - * capacity value; a SynchronousQueue else. + * capacity value; a SynchronousQueue otherwise. * @param queueCapacity the specified queue capacity * @return the BlockingQueue instance * @see java.util.concurrent.LinkedBlockingQueue @@ -360,16 +388,10 @@ public void execute(Runnable task) { executor.execute(task); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } - @Deprecated - @Override - public void execute(Runnable task, long startTimeout) { - execute(task); - } - @Override public Future submit(Runnable task) { ExecutorService executor = getThreadPoolExecutor(); @@ -377,7 +399,7 @@ public Future submit(Runnable task) { return executor.submit(task); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -388,7 +410,7 @@ public Future submit(Callable task) { return executor.submit(task); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -401,7 +423,7 @@ public ListenableFuture submitListenable(Runnable task) { return future; } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -414,7 +436,7 @@ public ListenableFuture submitListenable(Callable task) { return future; } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -428,4 +450,11 @@ protected void cancelRemainingTask(Runnable task) { } } + @Override + protected void initiateEarlyShutdown() { + if (this.strictEarlyShutdown) { + super.initiateEarlyShutdown(); + } + } + } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index bad07b6ee863..1783220223d8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,8 +46,16 @@ import org.springframework.util.concurrent.ListenableFutureTask; /** - * Implementation of Spring's {@link TaskScheduler} interface, wrapping - * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor}. + * A standard implementation of Spring's {@link TaskScheduler} interface, wrapping + * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor} and providing + * all applicable configuration options for it. The default number of scheduler + * threads is 1; a higher number can be configured through {@link #setPoolSize}. + * + *

This is Spring's traditional scheduler variant, staying as close as possible to + * {@link java.util.concurrent.ScheduledExecutorService} semantics. Task execution happens + * on the scheduler thread(s) rather than on separate execution threads. As a consequence, + * a {@link ScheduledFuture} handle (e.g. from {@link #schedule(Runnable, Instant)}) + * represents the actual completion of the provided task (or series of repeated tasks). * * @author Juergen Hoeller * @author Mark Fisher @@ -58,6 +66,8 @@ * @see #setExecuteExistingDelayedTasksAfterShutdownPolicy * @see #setThreadFactory * @see #setErrorHandler + * @see ThreadPoolTaskExecutor + * @see SimpleAsyncTaskScheduler */ @SuppressWarnings({"serial", "deprecation"}) public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport @@ -158,6 +168,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -202,7 +213,16 @@ protected ExecutorService initializeExecutor( protected ScheduledExecutorService createExecutor( int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { - return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler); + return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler) { + @Override + protected void beforeExecute(Thread thread, Runnable task) { + ThreadPoolTaskScheduler.this.beforeExecute(thread, task); + } + @Override + protected void afterExecute(Runnable task, Throwable ex) { + ThreadPoolTaskScheduler.this.afterExecute(task, ex); + } + }; } /** @@ -281,16 +301,10 @@ public void execute(Runnable task) { executor.execute(errorHandlingTask(task, false)); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } - @Deprecated - @Override - public void execute(Runnable task, long startTimeout) { - execute(task); - } - @Override public Future submit(Runnable task) { ExecutorService executor = getScheduledExecutor(); @@ -298,7 +312,7 @@ public Future submit(Runnable task) { return executor.submit(errorHandlingTask(task, false)); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -314,7 +328,7 @@ public Future submit(Callable task) { return executor.submit(taskToUse); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -327,7 +341,7 @@ public ListenableFuture submitListenable(Runnable task) { return listenableFuture; } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -340,7 +354,7 @@ public ListenableFuture submitListenable(Callable task) { return listenableFuture; } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -376,7 +390,7 @@ public ScheduledFuture schedule(Runnable task, Trigger trigger) { return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule(); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -388,7 +402,7 @@ public ScheduledFuture schedule(Runnable task, Instant startTime) { return executor.schedule(errorHandlingTask(task, false), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -401,7 +415,7 @@ public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, NANO.convert(initialDelay), NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -413,7 +427,7 @@ public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { 0, NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -426,7 +440,7 @@ public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTim NANO.convert(initialDelay), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } @@ -438,7 +452,7 @@ public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) 0, NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(executor, task, ex); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/DelayedTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/DelayedTask.java new file mode 100644 index 000000000000..497db7b11085 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/DelayedTask.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.config; + +import java.time.Duration; + +import org.springframework.util.Assert; + +/** + * {@link Task} implementation defining a {@code Runnable} with an initial delay. + * + * @author Juergen Hoeller + * @since 6.1 + */ +public class DelayedTask extends Task { + + private final Duration initialDelay; + + + /** + * Create a new {@code DelayedTask}. + * @param runnable the underlying task to execute + * @param initialDelay the initial delay before execution of the task + */ + public DelayedTask(Runnable runnable, Duration initialDelay) { + super(runnable); + Assert.notNull(initialDelay, "InitialDelay must not be null"); + this.initialDelay = initialDelay; + } + + /** + * Copy constructor. + */ + DelayedTask(DelayedTask task) { + super(task.getRunnable()); + Assert.notNull(task, "DelayedTask must not be null"); + this.initialDelay = task.getInitialDelayDuration(); + } + + + /** + * Return the initial delay before first execution of the task. + */ + public Duration getInitialDelayDuration() { + return this.initialDelay; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java index 7c53292786ab..1987aba7b9d4 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-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. @@ -61,22 +61,13 @@ private void configureRejectionPolicy(Element element, BeanDefinitionBuilder bui return; } String prefix = "java.util.concurrent.ThreadPoolExecutor."; - String policyClassName; - if (rejectionPolicy.equals("ABORT")) { - policyClassName = prefix + "AbortPolicy"; - } - else if (rejectionPolicy.equals("CALLER_RUNS")) { - policyClassName = prefix + "CallerRunsPolicy"; - } - else if (rejectionPolicy.equals("DISCARD")) { - policyClassName = prefix + "DiscardPolicy"; - } - else if (rejectionPolicy.equals("DISCARD_OLDEST")) { - policyClassName = prefix + "DiscardOldestPolicy"; - } - else { - policyClassName = rejectionPolicy; - } + String policyClassName = switch (rejectionPolicy) { + case "ABORT" -> prefix + "AbortPolicy"; + case "CALLER_RUNS" -> prefix + "CallerRunsPolicy"; + case "DISCARD" -> prefix + "DiscardPolicy"; + case "DISCARD_OLDEST" -> prefix + "DiscardOldestPolicy"; + default -> rejectionPolicy; + }; builder.addPropertyValue("rejectedExecutionHandler", new RootBeanDefinition(policyClassName)); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java index 9e4e28190e5d..fffac2b367e9 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java index 32a2130ef96b..782053fac875 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,10 @@ * @see ScheduledTaskRegistrar#addFixedRateTask(IntervalTask) * @see ScheduledTaskRegistrar#addFixedDelayTask(IntervalTask) */ -public class IntervalTask extends Task { +public class IntervalTask extends DelayedTask { private final Duration interval; - private final Duration initialDelay; - /** * Create a new {@code IntervalTask}. @@ -79,23 +77,17 @@ public IntervalTask(Runnable runnable, Duration interval) { * @since 6.0 */ public IntervalTask(Runnable runnable, Duration interval, Duration initialDelay) { - super(runnable); + super(runnable, initialDelay); Assert.notNull(interval, "Interval must not be null"); - Assert.notNull(initialDelay, "InitialDelay must not be null"); - this.interval = interval; - this.initialDelay = initialDelay; } /** * Copy constructor. */ IntervalTask(IntervalTask task) { - super(task.getRunnable()); - Assert.notNull(task, "IntervalTask must not be null"); - + super(task); this.interval = task.getIntervalDuration(); - this.initialDelay = task.getInitialDelayDuration(); } @@ -122,15 +114,16 @@ public Duration getIntervalDuration() { */ @Deprecated(since = "6.0") public long getInitialDelay() { - return this.initialDelay.toMillis(); + return getInitialDelayDuration().toMillis(); } /** * Return the initial delay before first execution of the task. * @since 6.0 */ + @Override public Duration getInitialDelayDuration() { - return this.initialDelay; + return super.getInitialDelayDuration(); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/OneTimeTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/OneTimeTask.java new file mode 100644 index 000000000000..e71d8d91ca61 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/OneTimeTask.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.config; + +import java.time.Duration; + +/** + * {@link Task} implementation defining a {@code Runnable} with an initial delay. + * + * @author Juergen Hoeller + * @since 6.1 + * @see ScheduledTaskRegistrar#addOneTimeTask(DelayedTask) + */ +public class OneTimeTask extends DelayedTask { + + /** + * Create a new {@code DelayedTask}. + * @param runnable the underlying task to execute + * @param initialDelay the initial delay before execution of the task + */ + public OneTimeTask(Runnable runnable, Duration initialDelay) { + super(runnable, initialDelay); + } + + OneTimeTask(DelayedTask task) { + super(task); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java index 657c7535f5d3..02cadb58f5ca 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -28,6 +28,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import io.micrometer.observation.ObservationRegistry; + import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.lang.Nullable; @@ -53,6 +55,7 @@ * @author Tobias Montagna-Hay * @author Sam Brannen * @author Arjen Poutsma + * @author Brian Clozel * @since 3.0 * @see org.springframework.scheduling.annotation.EnableAsync * @see org.springframework.scheduling.annotation.SchedulingConfigurer @@ -77,6 +80,9 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing @Nullable private ScheduledExecutorService localExecutor; + @Nullable + private ObservationRegistry observationRegistry; + @Nullable private List triggerTasks; @@ -89,6 +95,9 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing @Nullable private List fixedDelayTasks; + @Nullable + private List oneTimeTasks; + private final Map unresolvedTasks = new HashMap<>(16); private final Set scheduledTasks = new LinkedHashSet<>(16); @@ -130,6 +139,22 @@ public TaskScheduler getScheduler() { return this.taskScheduler; } + /** + * Configure an {@link ObservationRegistry} to record observations for scheduled tasks. + * @since 6.1 + */ + public void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + /** + * Return the {@link ObservationRegistry} for this registrar. + * @since 6.1 + */ + @Nullable + public ObservationRegistry getObservationRegistry() { + return this.observationRegistry; + } /** * Specify triggered tasks as a Map of Runnables (the tasks) and Trigger objects @@ -296,7 +321,7 @@ public void addCronTask(CronTask task) { */ @Deprecated(since = "6.0") public void addFixedRateTask(Runnable task, long interval) { - addFixedRateTask(new IntervalTask(task, Duration.ofMillis(interval))); + addFixedRateTask(new IntervalTask(task, interval)); } /** @@ -312,6 +337,7 @@ public void addFixedRateTask(Runnable task, Duration interval) { * Add a fixed-rate {@link IntervalTask}. * @since 3.2 * @see TaskScheduler#scheduleAtFixedRate(Runnable, Duration) + * @see FixedRateTask */ public void addFixedRateTask(IntervalTask task) { if (this.fixedRateTasks == null) { @@ -325,8 +351,8 @@ public void addFixedRateTask(IntervalTask task) { * @deprecated as of 6.0, in favor of {@link #addFixedDelayTask(Runnable, Duration)} */ @Deprecated(since = "6.0") - public void addFixedDelayTask(Runnable task, long delay) { - addFixedDelayTask(new IntervalTask(task, Duration.ofMillis(delay))); + public void addFixedDelayTask(Runnable task, long interval) { + addFixedDelayTask(new IntervalTask(task, interval)); } /** @@ -334,14 +360,15 @@ public void addFixedDelayTask(Runnable task, long delay) { * @since 6.0 * @see TaskScheduler#scheduleWithFixedDelay(Runnable, Duration) */ - public void addFixedDelayTask(Runnable task, Duration delay) { - addFixedDelayTask(new IntervalTask(task, delay)); + public void addFixedDelayTask(Runnable task, Duration interval) { + addFixedDelayTask(new IntervalTask(task, interval)); } /** * Add a fixed-delay {@link IntervalTask}. * @since 3.2 * @see TaskScheduler#scheduleWithFixedDelay(Runnable, Duration) + * @see FixedDelayTask */ public void addFixedDelayTask(IntervalTask task) { if (this.fixedDelayTasks == null) { @@ -350,6 +377,28 @@ public void addFixedDelayTask(IntervalTask task) { this.fixedDelayTasks.add(task); } + /** + * Add a Runnable task to be triggered once after the given initial delay. + * @since 6.1 + * @see TaskScheduler#schedule(Runnable, Instant) + */ + public void addOneTimeTask(Runnable task, Duration initialDelay) { + addOneTimeTask(new OneTimeTask(task, initialDelay)); + } + + /** + * Add a one-time {@link DelayedTask}. + * @since 6.1 + * @see TaskScheduler#schedule(Runnable, Instant) + * @see OneTimeTask + */ + public void addOneTimeTask(DelayedTask task) { + if (this.oneTimeTasks == null) { + this.oneTimeTasks = new ArrayList<>(); + } + this.oneTimeTasks.add(task); + } + /** * Return whether this {@code ScheduledTaskRegistrar} has any tasks registered. @@ -359,7 +408,8 @@ public boolean hasTasks() { return (!CollectionUtils.isEmpty(this.triggerTasks) || !CollectionUtils.isEmpty(this.cronTasks) || !CollectionUtils.isEmpty(this.fixedRateTasks) || - !CollectionUtils.isEmpty(this.fixedDelayTasks)); + !CollectionUtils.isEmpty(this.fixedDelayTasks) || + !CollectionUtils.isEmpty(this.oneTimeTasks)); } @@ -410,6 +460,16 @@ protected void scheduleTasks() { } } } + if (this.oneTimeTasks != null) { + for (DelayedTask task : this.oneTimeTasks) { + if (task instanceof OneTimeTask oneTimeTask) { + addScheduledTask(scheduleOneTimeTask(oneTimeTask)); + } + else { + addScheduledTask(scheduleOneTimeTask(new OneTimeTask(task))); + } + } + } } private void addScheduledTask(@Nullable ScheduledTask task) { @@ -536,6 +596,32 @@ public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) { return (newTask ? scheduledTask : null); } + /** + * Schedule the specified one-time task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 6.1 + */ + @Nullable + public ScheduledTask scheduleOneTimeTask(OneTimeTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(task); + newTask = true; + } + if (this.taskScheduler != null) { + Instant startTime = this.taskScheduler.getClock().instant().plus(task.getInitialDelayDuration()); + scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), startTime); + } + else { + addOneTimeTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + /** * Return all locally registered tasks that have been scheduled by this registrar. diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskSchedulerRouter.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskSchedulerRouter.java new file mode 100644 index 000000000000..21898b50efa2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskSchedulerRouter.java @@ -0,0 +1,265 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.config; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; +import org.springframework.beans.factory.config.NamedBeanHolder; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; +import org.springframework.util.function.SingletonSupplier; + +/** + * A routing implementation of the {@link TaskScheduler} interface, + * delegating to a target scheduler based on an identified qualifier + * or using a default scheduler otherwise. + * + * @author Juergen Hoeller + * @since 6.1 + * @see SchedulingAwareRunnable#getQualifier() + */ +public class TaskSchedulerRouter implements TaskScheduler, BeanNameAware, BeanFactoryAware, DisposableBean { + + /** + * The default name of the {@link TaskScheduler} bean to pick up: {@value}. + *

Note that the initial lookup happens by type; this is just the fallback + * in case of multiple scheduler beans found in the context. + */ + public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; + + + protected static final Log logger = LogFactory.getLog(TaskSchedulerRouter.class); + + @Nullable + private String beanName; + + @Nullable + private BeanFactory beanFactory; + + @Nullable + private StringValueResolver embeddedValueResolver; + + private final Supplier defaultScheduler = SingletonSupplier.of(this::determineDefaultScheduler); + + @Nullable + private volatile ScheduledExecutorService localExecutor; + + + /** + * The bean name for this router, or the bean name of the containing + * bean if the router instance is internally held. + */ + @Override + public void setBeanName(@Nullable String name) { + this.beanName = name; + } + + /** + * The bean factory for scheduler lookups. + */ + @Override + public void setBeanFactory(@Nullable BeanFactory beanFactory) { + this.beanFactory = beanFactory; + if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { + this.embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); + } + } + + + @Override + @Nullable + public ScheduledFuture schedule(Runnable task, Trigger trigger) { + return determineTargetScheduler(task).schedule(task, trigger); + } + + @Override + public ScheduledFuture schedule(Runnable task, Instant startTime) { + return determineTargetScheduler(task).schedule(task, startTime); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + return determineTargetScheduler(task).scheduleAtFixedRate(task, startTime, period); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + return determineTargetScheduler(task).scheduleAtFixedRate(task, period); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + return determineTargetScheduler(task).scheduleWithFixedDelay(task, startTime, delay); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + return determineTargetScheduler(task).scheduleWithFixedDelay(task, delay); + } + + + protected TaskScheduler determineTargetScheduler(Runnable task) { + String qualifier = determineQualifier(task); + if (this.embeddedValueResolver != null && StringUtils.hasLength(qualifier)) { + qualifier = this.embeddedValueResolver.resolveStringValue(qualifier); + } + if (StringUtils.hasLength(qualifier)) { + return determineQualifiedScheduler(qualifier); + } + else { + return this.defaultScheduler.get(); + } + } + + @Nullable + protected String determineQualifier(Runnable task) { + return (task instanceof SchedulingAwareRunnable sar ? sar.getQualifier() : null); + } + + protected TaskScheduler determineQualifiedScheduler(String qualifier) { + Assert.state(this.beanFactory != null, "BeanFactory must be set to find qualified scheduler"); + try { + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, TaskScheduler.class, qualifier); + } + catch (NoSuchBeanDefinitionException | BeanNotOfRequiredTypeException ex) { + return new ConcurrentTaskScheduler(BeanFactoryAnnotationUtils.qualifiedBeanOfType( + this.beanFactory, ScheduledExecutorService.class, qualifier)); + } + } + + protected TaskScheduler determineDefaultScheduler() { + Assert.state(this.beanFactory != null, "BeanFactory must be set to find default scheduler"); + try { + // Search for TaskScheduler bean... + return resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false); + } + catch (NoUniqueBeanDefinitionException ex) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find unique TaskScheduler bean - attempting to resolve by name: " + + ex.getMessage()); + } + try { + return resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true); + } + catch (NoSuchBeanDefinitionException ex2) { + if (logger.isInfoEnabled()) { + logger.info("More than one TaskScheduler bean exists within the context, and " + + "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + + "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + + "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + + ex.getBeanNamesFound()); + } + } + } + catch (NoSuchBeanDefinitionException ex) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find default TaskScheduler bean - attempting to find ScheduledExecutorService: " + + ex.getMessage()); + } + // Search for ScheduledExecutorService bean next... + try { + return new ConcurrentTaskScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false)); + } + catch (NoUniqueBeanDefinitionException ex2) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find unique ScheduledExecutorService bean - attempting to resolve by name: " + + ex2.getMessage()); + } + try { + return new ConcurrentTaskScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true)); + } + catch (NoSuchBeanDefinitionException ex3) { + if (logger.isInfoEnabled()) { + logger.info("More than one ScheduledExecutorService bean exists within the context, and " + + "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + + "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + + "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + + ex2.getBeanNamesFound()); + } + } + } + catch (NoSuchBeanDefinitionException ex2) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find default ScheduledExecutorService bean - falling back to default: " + + ex2.getMessage()); + } + logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing"); + } + } + ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor(); + this.localExecutor = localExecutor; + return new ConcurrentTaskScheduler(localExecutor); + } + + private T resolveSchedulerBean(BeanFactory beanFactory, Class schedulerType, boolean byName) { + if (byName) { + T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType); + if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory cbf) { + cbf.registerDependentBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName); + } + return scheduler; + } + else if (beanFactory instanceof AutowireCapableBeanFactory acbf) { + NamedBeanHolder holder = acbf.resolveNamedBean(schedulerType); + if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory cbf) { + cbf.registerDependentBean(holder.getBeanName(), this.beanName); + } + return holder.getBeanInstance(); + } + else { + return beanFactory.getBean(schedulerType); + } + } + + + /** + * Destroy the local default executor, if any. + */ + @Override + public void destroy() { + ScheduledExecutorService localExecutor = this.localExecutor; + if (localExecutor != null) { + localExecutor.shutdownNow(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java index ac877a72a460..3ef274e63c5c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,16 +29,14 @@ * Created using the {@code parse*} methods. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.3 */ final class BitsCronField extends CronField { - private static final long MASK = 0xFFFFFFFFFFFFFFFFL; - - - @Nullable - private static BitsCronField zeroNanos = null; + public static final BitsCronField ZERO_NANOS = forZeroNanos(); + private static final long MASK = 0xFFFFFFFFFFFFFFFFL; // we store at most 60 bits, for seconds and minutes, so a 64-bit long suffices private long bits; @@ -48,16 +46,14 @@ private BitsCronField(Type type) { super(type); } + /** * Return a {@code BitsCronField} enabled for 0 nanoseconds. */ - public static BitsCronField zeroNanos() { - if (zeroNanos == null) { - BitsCronField field = new BitsCronField(Type.NANO); - field.setBit(0); - zeroNanos = field; - } - return zeroNanos; + private static BitsCronField forZeroNanos() { + BitsCronField field = new BitsCronField(Type.NANO); + field.setBit(0); + return field; } /** @@ -108,7 +104,6 @@ public static BitsCronField parseDaysOfWeek(String value) { return result; } - private static BitsCronField parseDate(String value, BitsCronField.Type type) { if (value.equals("?")) { value = "*"; @@ -174,6 +169,7 @@ private static ValueRange parseRange(String value, Type type) { } } + @Nullable @Override public > T nextOrSame(T temporal) { @@ -217,7 +213,6 @@ private int nextSetBit(int fromIndex) { else { return -1; } - } private void setBits(ValueRange range) { @@ -247,23 +242,19 @@ private void setBit(int index) { } private void clearBit(int index) { - this.bits &= ~(1L << index); + this.bits &= ~(1L << index); } + @Override - public int hashCode() { - return Long.hashCode(this.bits); + public boolean equals(Object other) { + return (this == other || (other instanceof BitsCronField that && + type() == that.type() && this.bits == that.bits)); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!(o instanceof BitsCronField other)) { - return false; - } - return type() == other.type() && this.bits == other.bits; + public int hashCode() { + return Long.hashCode(this.bits); } @Override diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index 0e6008de6253..fdc7cb96dc28 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,14 @@ * crontab expression * that can calculate the next time it matches. * - *

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

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

Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Arjen Poutsma * @since 5.3 @@ -168,7 +173,7 @@ private CronExpression(CronField seconds, CronField minutes, CronField hours, * the cron format */ public static CronExpression parse(String expression) { - Assert.hasLength(expression, "Expression string must not be empty"); + Assert.hasLength(expression, "Expression must not be empty"); expression = resolveMacros(expression); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index 99d940613e5c..3124cc25b5e5 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,17 +29,24 @@ /** * Single field in a cron pattern. Created using the {@code parse*} methods, - * main and only entry point is {@link #nextOrSame(Temporal)}. + * the main and only entry point is {@link #nextOrSame(Temporal)}. + * + *

Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Arjen Poutsma * @since 5.3 */ abstract class CronField { - private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", - "OCT", "NOV", "DEC"}; + private static final String[] MONTHS = new String[] + {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; - private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; + private static final String[] DAYS = new String[] + {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; private final Type type; @@ -48,11 +55,12 @@ protected CronField(Type type) { this.type = type; } + /** * Return a {@code CronField} enabled for 0 nanoseconds. */ public static CronField zeroNanos() { - return BitsCronField.zeroNanos(); + return BitsCronField.ZERO_NANOS; } /** @@ -169,6 +177,7 @@ protected static > T cast(Temporal te * day-of-month, month, day-of-week. */ protected enum Type { + NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS), SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND), MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), @@ -177,21 +186,18 @@ protected enum Type { MONTH(ChronoField.MONTH_OF_YEAR, ChronoUnit.YEARS, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoUnit.WEEKS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); - private final ChronoField field; private final ChronoUnit higherOrder; private final ChronoField[] lowerOrders; - Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) { this.field = field; this.higherOrder = higherOrder; this.lowerOrders = lowerOrders; } - /** * Return the value of this type for the given temporal. * @return the value of this type diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java deleted file mode 100644 index 985ed508f15f..000000000000 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java +++ /dev/null @@ -1,455 +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.scheduling.support; - -import java.util.ArrayList; -import java.util.BitSet; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.TimeZone; - -import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; - -/** - * Date sequence generator for a - * Crontab pattern, - * allowing clients to specify a pattern that the sequence matches. - * - *

The pattern is a list of six single space-separated fields: representing - * second, minute, hour, day, month, weekday. Month and weekday names can be - * given as the first three letters of the English names. - * - *

Example patterns: - *

    - *
  • "0 0 * * * *" = the top of every hour of every day.
  • - *
  • "*/10 * * * * *" = every ten seconds.
  • - *
  • "0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day.
  • - *
  • "0 0 6,19 * * *" = 6:00 AM and 7:00 PM every day.
  • - *
  • "0 0/30 8-10 * * *" = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
  • - *
  • "0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays
  • - *
  • "0 0 0 25 12 ?" = every Christmas Day at midnight
  • - *
- * - * @author Dave Syer - * @author Juergen Hoeller - * @author Ruslan Sibgatullin - * @since 3.0 - * @see CronTrigger - * @deprecated as of 5.3, in favor of {@link CronExpression} - */ -@Deprecated(since = "5.3", forRemoval = true) -public class CronSequenceGenerator { - - private final String expression; - - @Nullable - private final TimeZone timeZone; - - private final BitSet months = new BitSet(12); - - private final BitSet daysOfMonth = new BitSet(31); - - private final BitSet daysOfWeek = new BitSet(7); - - private final BitSet hours = new BitSet(24); - - private final BitSet minutes = new BitSet(60); - - private final BitSet seconds = new BitSet(60); - - - /** - * Construct a {@code CronSequenceGenerator} from the pattern provided, - * using the default {@link TimeZone}. - * @param expression a space-separated list of time fields - * @throws IllegalArgumentException if the pattern cannot be parsed - * @see java.util.TimeZone#getDefault() - * @deprecated as of 5.3, in favor of {@link CronExpression#parse(String)} - */ - @Deprecated(since = "5.3", forRemoval = true) - public CronSequenceGenerator(String expression) { - this(expression, TimeZone.getDefault()); - } - - /** - * Construct a {@code CronSequenceGenerator} from the pattern provided, - * using the specified {@link TimeZone}. - * @param expression a space-separated list of time fields - * @param timeZone the TimeZone to use for generated trigger times - * @throws IllegalArgumentException if the pattern cannot be parsed - * @deprecated as of 5.3, in favor of {@link CronExpression#parse(String)} - */ - @Deprecated - public CronSequenceGenerator(String expression, TimeZone timeZone) { - this.expression = expression; - this.timeZone = timeZone; - parse(expression); - } - - private CronSequenceGenerator(String expression, String[] fields) { - this.expression = expression; - this.timeZone = null; - doParse(fields); - } - - - /** - * Return the cron pattern that this sequence generator has been built for. - */ - String getExpression() { - return this.expression; - } - - - /** - * Get the next {@link Date} in the sequence matching the Cron pattern and - * after the value provided. The return value will have a whole number of - * seconds, and will be after the input value. - * @param date a seed value - * @return the next value matching the pattern - */ - public Date next(Date date) { - /* - The plan: - - 1 Start with whole second (rounding up if necessary) - - 2 If seconds match move on, otherwise find the next match: - 2.1 If next match is in the next minute then roll forwards - - 3 If minute matches move on, otherwise find the next match - 3.1 If next match is in the next hour then roll forwards - 3.2 Reset the seconds and go to 2 - - 4 If hour matches move on, otherwise find the next match - 4.1 If next match is in the next day then roll forwards, - 4.2 Reset the minutes and seconds and go to 2 - */ - - Calendar calendar = new GregorianCalendar(); - calendar.setTimeZone(this.timeZone); - calendar.setTime(date); - - // First, just reset the milliseconds and try to calculate from there... - calendar.set(Calendar.MILLISECOND, 0); - long originalTimestamp = calendar.getTimeInMillis(); - doNext(calendar, calendar.get(Calendar.YEAR)); - - if (calendar.getTimeInMillis() == originalTimestamp) { - // We arrived at the original timestamp - round up to the next whole second and try again... - calendar.add(Calendar.SECOND, 1); - doNext(calendar, calendar.get(Calendar.YEAR)); - } - - return calendar.getTime(); - } - - private void doNext(Calendar calendar, int dot) { - List resets = new ArrayList<>(); - - int second = calendar.get(Calendar.SECOND); - List emptyList = Collections.emptyList(); - int updateSecond = findNext(this.seconds, second, calendar, Calendar.SECOND, Calendar.MINUTE, emptyList); - if (second == updateSecond) { - resets.add(Calendar.SECOND); - } - - int minute = calendar.get(Calendar.MINUTE); - int updateMinute = findNext(this.minutes, minute, calendar, Calendar.MINUTE, Calendar.HOUR_OF_DAY, resets); - if (minute == updateMinute) { - resets.add(Calendar.MINUTE); - } - else { - doNext(calendar, dot); - } - - int hour = calendar.get(Calendar.HOUR_OF_DAY); - int updateHour = findNext(this.hours, hour, calendar, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_WEEK, resets); - if (hour == updateHour) { - resets.add(Calendar.HOUR_OF_DAY); - } - else { - doNext(calendar, dot); - } - - int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); - int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); - int updateDayOfMonth = findNextDay(calendar, this.daysOfMonth, dayOfMonth, this.daysOfWeek, dayOfWeek, resets); - if (dayOfMonth == updateDayOfMonth) { - resets.add(Calendar.DAY_OF_MONTH); - } - else { - doNext(calendar, dot); - } - - int month = calendar.get(Calendar.MONTH); - int updateMonth = findNext(this.months, month, calendar, Calendar.MONTH, Calendar.YEAR, resets); - if (month != updateMonth) { - if (calendar.get(Calendar.YEAR) - dot > 4) { - throw new IllegalArgumentException("Invalid cron expression \"" + this.expression + - "\" led to runaway search for next trigger"); - } - doNext(calendar, dot); - } - - } - - private int findNextDay(Calendar calendar, BitSet daysOfMonth, int dayOfMonth, BitSet daysOfWeek, int dayOfWeek, - List resets) { - - int count = 0; - int max = 366; - // the DAY_OF_WEEK values in java.util.Calendar start with 1 (Sunday), - // but in the cron pattern, they start with 0, so we subtract 1 here - while ((!daysOfMonth.get(dayOfMonth) || !daysOfWeek.get(dayOfWeek - 1)) && count++ < max) { - calendar.add(Calendar.DAY_OF_MONTH, 1); - dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); - dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); - reset(calendar, resets); - } - if (count >= max) { - throw new IllegalArgumentException("Overflow in day for expression \"" + this.expression + "\""); - } - return dayOfMonth; - } - - /** - * Search the bits provided for the next set bit after the value provided, - * and reset the calendar. - * @param bits a {@link BitSet} representing the allowed values of the field - * @param value the current value of the field - * @param calendar the calendar to increment as we move through the bits - * @param field the field to increment in the calendar (@see - * {@link Calendar} for the static constants defining valid fields) - * @param lowerOrders the Calendar field ids that should be reset (i.e. the - * ones of lower significance than the field of interest) - * @return the value of the calendar field that is next in the sequence - */ - private int findNext(BitSet bits, int value, Calendar calendar, int field, int nextField, List lowerOrders) { - int nextValue = bits.nextSetBit(value); - // roll over if needed - if (nextValue == -1) { - calendar.add(nextField, 1); - reset(calendar, Collections.singletonList(field)); - nextValue = bits.nextSetBit(0); - } - if (nextValue != value) { - calendar.set(field, nextValue); - reset(calendar, lowerOrders); - } - return nextValue; - } - - /** - * Reset the calendar setting all the fields provided to zero. - */ - private void reset(Calendar calendar, List fields) { - for (int field : fields) { - calendar.set(field, field == Calendar.DAY_OF_MONTH ? 1 : 0); - } - } - - - // Parsing logic invoked by the constructor - - /** - * Parse the given pattern expression. - */ - private void parse(String expression) throws IllegalArgumentException { - String[] fields = StringUtils.tokenizeToStringArray(expression, " "); - if (!areValidCronFields(fields)) { - throw new IllegalArgumentException(String.format( - "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression)); - } - doParse(fields); - } - - private void doParse(String[] fields) { - setNumberHits(this.seconds, fields[0], 0, 60); - setNumberHits(this.minutes, fields[1], 0, 60); - setNumberHits(this.hours, fields[2], 0, 24); - setDaysOfMonth(this.daysOfMonth, fields[3]); - setMonths(this.months, fields[4]); - setDays(this.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8); - - if (this.daysOfWeek.get(7)) { - // Sunday can be represented as 0 or 7 - this.daysOfWeek.set(0); - this.daysOfWeek.clear(7); - } - } - - /** - * Replace the values in the comma-separated list (case-insensitive) - * with their index in the list. - * @return a new String with the values from the list replaced - */ - private String replaceOrdinals(String value, String commaSeparatedList) { - String[] list = StringUtils.commaDelimitedListToStringArray(commaSeparatedList); - for (int i = 0; i < list.length; i++) { - String item = list[i].toUpperCase(); - value = StringUtils.replace(value.toUpperCase(), item, "" + i); - } - return value; - } - - private void setDaysOfMonth(BitSet bits, String field) { - int max = 31; - // Days of month start with 1 (in Cron and Calendar) so add one - setDays(bits, field, max + 1); - // ... and remove it from the front - bits.clear(0); - } - - private void setDays(BitSet bits, String field, int max) { - if (field.contains("?")) { - field = "*"; - } - setNumberHits(bits, field, 0, max); - } - - private void setMonths(BitSet bits, String value) { - int max = 12; - value = replaceOrdinals(value, "FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC"); - BitSet months = new BitSet(13); - // Months start with 1 in Cron and 0 in Calendar, so push the values first into a longer bit set - setNumberHits(months, value, 1, max + 1); - // ... and then rotate it to the front of the months - for (int i = 1; i <= max; i++) { - if (months.get(i)) { - bits.set(i - 1); - } - } - } - - private void setNumberHits(BitSet bits, String value, int min, int max) { - String[] fields = StringUtils.delimitedListToStringArray(value, ","); - for (String field : fields) { - if (!field.contains("/")) { - // Not an incrementer so it must be a range (possibly empty) - int[] range = getRange(field, min, max); - bits.set(range[0], range[1] + 1); - } - else { - String[] split = StringUtils.delimitedListToStringArray(field, "/"); - if (split.length > 2) { - throw new IllegalArgumentException("Incrementer has more than two fields: '" + - field + "' in expression \"" + this.expression + "\""); - } - int[] range = getRange(split[0], min, max); - if (!split[0].contains("-")) { - range[1] = max - 1; - } - int delta = Integer.parseInt(split[1]); - if (delta <= 0) { - throw new IllegalArgumentException("Incrementer delta must be 1 or higher: '" + - field + "' in expression \"" + this.expression + "\""); - } - for (int i = range[0]; i <= range[1]; i += delta) { - bits.set(i); - } - } - } - } - - private int[] getRange(String field, int min, int max) { - int[] result = new int[2]; - if (field.contains("*")) { - result[0] = min; - result[1] = max - 1; - return result; - } - if (!field.contains("-")) { - result[0] = result[1] = Integer.parseInt(field); - } - else { - String[] split = StringUtils.delimitedListToStringArray(field, "-"); - if (split.length > 2) { - throw new IllegalArgumentException("Range has more than two fields: '" + - field + "' in expression \"" + this.expression + "\""); - } - result[0] = Integer.parseInt(split[0]); - result[1] = Integer.parseInt(split[1]); - } - if (result[0] >= max || result[1] >= max) { - throw new IllegalArgumentException("Range exceeds maximum (" + max + "): '" + - field + "' in expression \"" + this.expression + "\""); - } - if (result[0] < min || result[1] < min) { - throw new IllegalArgumentException("Range less than minimum (" + min + "): '" + - field + "' in expression \"" + this.expression + "\""); - } - if (result[0] > result[1]) { - throw new IllegalArgumentException("Invalid inverted range: '" + field + - "' in expression \"" + this.expression + "\""); - } - return result; - } - - - /** - * Determine whether the specified expression represents a valid cron pattern. - * @param expression the expression to evaluate - * @return {@code true} if the given expression is a valid cron expression - * @since 4.3 - */ - public static boolean isValidExpression(@Nullable String expression) { - if (expression == null) { - return false; - } - String[] fields = StringUtils.tokenizeToStringArray(expression, " "); - if (!areValidCronFields(fields)) { - return false; - } - try { - new CronSequenceGenerator(expression, fields); - return true; - } - catch (IllegalArgumentException ex) { - return false; - } - } - - private static boolean areValidCronFields(@Nullable String[] fields) { - return (fields != null && fields.length == 6); - } - - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof CronSequenceGenerator that && - this.months.equals(that.months) && this.daysOfMonth.equals(that.daysOfMonth) && - this.daysOfWeek.equals(that.daysOfWeek) && this.hours.equals(that.hours) && - this.minutes.equals(that.minutes) && this.seconds.equals(that.seconds))); - } - - @Override - public int hashCode() { - return (17 * this.months.hashCode() + 29 * this.daysOfMonth.hashCode() + 37 * this.daysOfWeek.hashCode() + - 41 * this.hours.hashCode() + 53 * this.minutes.hashCode() + 61 * this.seconds.hashCode()); - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": " + this.expression; - } - -} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index 7bcb8ed2fa74..dc8c9a4ab527 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,14 @@ import org.springframework.util.Assert; /** - * {@link Trigger} implementation for cron expressions. - * Wraps a {@link CronExpression}. + * {@link Trigger} implementation for cron expressions. Wraps a + * {@link CronExpression} which parses according to common crontab conventions. + * + *

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

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

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

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

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

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

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

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

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

This variant makes up for missed trigger firings if the associated task + * has taken too long, scheduling a task for every original trigger firing. + * Such follow-up tasks may execute late but will never be skipped. + * @param expression a space-separated list of time fields, following cron + * expression conventions + * @param resumptionTimestamp the timestamp to resume from (the last-known + * scheduled timestamp), with every trigger in-between immediately firing + * to make up for every execution that would have happened in the meantime + * @since 6.1.3 + * @see #forFixedExecution + */ + public static CronTrigger resumeFixedExecution(String expression, Instant resumptionTimestamp) { + return new CronTrigger(expression) { + @Override + protected Instant determineLatestTimestamp(TriggerContext triggerContext) { + Instant scheduled = triggerContext.lastScheduledExecution(); + return (scheduled != null ? scheduled : super.determineLatestTimestamp(triggerContext)); + } + @Override + Instant determineInitialTimestamp(TriggerContext triggerContext) { + return resumptionTimestamp; + } + }; + } + } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java new file mode 100644 index 000000000000..4332c460b17b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.support; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; + +import org.springframework.util.StringUtils; + +import static org.springframework.scheduling.support.ScheduledTaskObservationDocumentation.LowCardinalityKeyNames; + +/** + * Default implementation for {@link ScheduledTaskObservationConvention}. + * @author Brian Clozel + * @since 6.1 + */ +public class DefaultScheduledTaskObservationConvention implements ScheduledTaskObservationConvention { + + private static final String DEFAULT_NAME = "tasks.scheduled.execution"; + + private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE); + + private static final KeyValue OUTCOME_SUCCESS = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "SUCCESS"); + + private static final KeyValue OUTCOME_ERROR = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "ERROR"); + + private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "UNKNOWN"); + + private static final KeyValue CODE_NAMESPACE_ANONYMOUS = KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, "ANONYMOUS"); + + @Override + public String getName() { + return DEFAULT_NAME; + } + + @Override + public String getContextualName(ScheduledTaskObservationContext context) { + return "task " + StringUtils.uncapitalize(context.getTargetClass().getSimpleName()) + + "." + context.getMethod().getName(); + } + + @Override + public KeyValues getLowCardinalityKeyValues(ScheduledTaskObservationContext context) { + return KeyValues.of(codeFunction(context), codeNamespace(context), exception(context), outcome(context)); + } + + protected KeyValue codeFunction(ScheduledTaskObservationContext context) { + return KeyValue.of(LowCardinalityKeyNames.CODE_FUNCTION, context.getMethod().getName()); + } + + protected KeyValue codeNamespace(ScheduledTaskObservationContext context) { + if (context.getTargetClass().getCanonicalName() != null) { + return KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, context.getTargetClass().getCanonicalName()); + } + return CODE_NAMESPACE_ANONYMOUS; + } + + protected KeyValue exception(ScheduledTaskObservationContext context) { + if (context.getError() != null) { + return KeyValue.of(LowCardinalityKeyNames.EXCEPTION, context.getError().getClass().getSimpleName()); + } + return EXCEPTION_NONE; + } + + protected KeyValue outcome(ScheduledTaskObservationContext context) { + if (context.getError() != null) { + return OUTCOME_ERROR; + } + if (!context.isComplete()) { + return OUTCOME_UNKNOWN; + } + return OUTCOME_SUCCESS; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java new file mode 100644 index 000000000000..c2733dd86ea7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.support; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CancellationException; +import java.util.concurrent.Delayed; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; + +/** + * A basic, no operation {@link TaskScheduler} implementation suitable + * for disabling scheduling, typically used for test setups. + * + *

Will accept any scheduling request but never actually execute it. + * + * @author Juergen Hoeller + * @since 6.1.3 + */ +public class NoOpTaskScheduler implements TaskScheduler { + + @Override + @Nullable + public ScheduledFuture schedule(Runnable task, Trigger trigger) { + Instant nextExecution = trigger.nextExecution(new SimpleTriggerContext(getClock())); + return (nextExecution != null ? new NoOpScheduledFuture<>() : null); + } + + @Override + public ScheduledFuture schedule(Runnable task, Instant startTime) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + return new NoOpScheduledFuture<>(); + } + + + private static class NoOpScheduledFuture implements ScheduledFuture { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return true; + } + + @Override + public boolean isCancelled() { + return true; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public V get() { + throw new CancellationException("No-op"); + } + + @Override + public V get(long timeout, TimeUnit unit) { + throw new CancellationException("No-op"); + } + + @Override + public long getDelay(TimeUnit unit) { + return 0; + } + + @Override + public int compareTo(Delayed other) { + return 0; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index d1f0a547071e..8c0873d15109 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,15 @@ /** * Extension of {@link CronField} for * Quartz-specific fields. - * - *

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

Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. + * * @author Arjen Poutsma * @since 5.3 */ @@ -61,8 +66,9 @@ private QuartzCronField(Type type, Type rollForwardType, TemporalAdjuster adjust this.rollForwardType = rollForwardType; } + /** - * Returns whether the given value is a Quartz day-of-month field. + * Determine whether the given value is a Quartz day-of-month field. */ public static boolean isQuartzDaysOfMonthField(String value) { return value.contains("L") || value.contains("W"); @@ -80,14 +86,14 @@ public static QuartzCronField parseDaysOfMonth(String value) { if (idx != 0) { throw new IllegalArgumentException("Unrecognized characters before 'L' in '" + value + "'"); } - else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" + else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" adjuster = lastWeekdayOfMonth(); } else { - if (value.length() == 1) { // "L" + if (value.length() == 1) { // "L" adjuster = lastDayOfMonth(); } - else { // "L-[0-9]+" + else { // "L-[0-9]+" int offset = Integer.parseInt(value, idx + 1, value.length(), 10); if (offset >= 0) { throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'"); @@ -105,7 +111,7 @@ else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" else if (idx != value.length() - 1) { throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'"); } - else { // "[0-9]+W" + else { // "[0-9]+W" int dayOfMonth = Integer.parseInt(value, 0, idx, 10); dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth); TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth); @@ -116,7 +122,7 @@ else if (idx != value.length() - 1) { } /** - * Returns whether the given value is a Quartz day-of-week field. + * Determine whether the given value is a Quartz day-of-week field. */ public static boolean isQuartzDaysOfWeekField(String value) { return value.contains("L") || value.contains("#"); @@ -138,7 +144,7 @@ public static QuartzCronField parseDaysOfWeek(String value) { if (idx == 0) { throw new IllegalArgumentException("No day-of-week before 'L' in '" + value + "'"); } - else { // "[0-7]L" + else { // "[0-7]L" DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx)); adjuster = lastInMonth(dayOfWeek); } @@ -160,7 +166,6 @@ else if (idx == value.length() - 1) { throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value + "' must be positive number "); } - TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek); return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value); } @@ -170,14 +175,13 @@ else if (idx == value.length() - 1) { private static DayOfWeek parseDayOfWeek(String value) { int dayOfWeek = Integer.parseInt(value); if (dayOfWeek == 0) { - dayOfWeek = 7; // cron is 0 based; java.time 1 based + dayOfWeek = 7; // cron is 0 based; java.time 1 based } try { return DayOfWeek.of(dayOfWeek); } catch (DateTimeException ex) { - String msg = ex.getMessage() + " '" + value + "'"; - throw new IllegalArgumentException(msg, ex); + throw new IllegalArgumentException(ex.getMessage() + " '" + value + "'", ex); } } @@ -216,10 +220,10 @@ private static TemporalAdjuster lastWeekdayOfMonth() { Temporal lastDom = adjuster.adjustInto(temporal); Temporal result; int dow = lastDom.get(ChronoField.DAY_OF_WEEK); - if (dow == 6) { // Saturday + if (dow == 6) { // Saturday result = lastDom.minus(1, ChronoUnit.DAYS); } - else if (dow == 7) { // Sunday + else if (dow == 7) { // Sunday result = lastDom.minus(2, ChronoUnit.DAYS); } else { @@ -256,10 +260,10 @@ private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) { int current = Type.DAY_OF_MONTH.get(temporal); DayOfWeek dayOfWeek = DayOfWeek.from(temporal); - if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday - (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before - (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after - (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd + if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday + (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before + (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after + (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd return temporal; } int count = 0; @@ -332,7 +336,9 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) { } } + @Override + @Nullable public > T nextOrSame(T temporal) { T result = adjust(temporal); if (result != null) { @@ -348,7 +354,6 @@ public > T nextOrSame(T temporal) { return result; } - @Nullable @SuppressWarnings("unchecked") private > T adjust(T temporal) { @@ -357,26 +362,19 @@ private > T adjust(T temporal) { @Override - public int hashCode() { - return this.value.hashCode(); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof QuartzCronField that && + type() == that.type() && this.value.equals(that.value))); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!(o instanceof QuartzCronField other)) { - return false; - } - return type() == other.type() && - this.value.equals(other.value); + public int hashCode() { + return this.value.hashCode(); } @Override public String toString() { return type() + " '" + this.value + "'"; - } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java index 4977226424b5..0f0b5d200e89 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,13 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; +import java.util.function.Supplier; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.util.ReflectionUtils; /** @@ -28,25 +34,52 @@ * assuming that an error strategy for Runnables is in place. * * @author Juergen Hoeller + * @author Brian Clozel * @since 3.0.6 * @see org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor */ -public class ScheduledMethodRunnable implements Runnable { +public class ScheduledMethodRunnable implements SchedulingAwareRunnable { + + private static final ScheduledTaskObservationConvention DEFAULT_CONVENTION = + new DefaultScheduledTaskObservationConvention(); private final Object target; private final Method method; + @Nullable + private final String qualifier; + + private final Supplier observationRegistrySupplier; + /** * Create a {@code ScheduledMethodRunnable} for the given target instance, * calling the specified method. * @param target the target instance to call the method on * @param method the target method to call + * @param qualifier a qualifier associated with this Runnable, + * e.g. for determining a scheduler to run this scheduled method on + * @param observationRegistrySupplier a supplier for the observation registry to use + * @since 6.1 */ - public ScheduledMethodRunnable(Object target, Method method) { + public ScheduledMethodRunnable(Object target, Method method, @Nullable String qualifier, + Supplier observationRegistrySupplier) { + this.target = target; this.method = method; + this.qualifier = qualifier; + this.observationRegistrySupplier = observationRegistrySupplier; + } + + /** + * Create a {@code ScheduledMethodRunnable} for the given target instance, + * calling the specified method. + * @param target the target instance to call the method on + * @param method the target method to call + */ + public ScheduledMethodRunnable(Object target, Method method) { + this(target, method, null, () -> ObservationRegistry.NOOP); } /** @@ -57,8 +90,7 @@ public ScheduledMethodRunnable(Object target, Method method) { * @throws NoSuchMethodException if the specified method does not exist */ public ScheduledMethodRunnable(Object target, String methodName) throws NoSuchMethodException { - this.target = target; - this.method = target.getClass().getMethod(methodName); + this(target, target.getClass().getMethod(methodName)); } @@ -76,12 +108,27 @@ public Method getMethod() { return this.method; } + @Override + @Nullable + public String getQualifier() { + return this.qualifier; + } + @Override public void run() { + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(this.target, this.method); + Observation observation = ScheduledTaskObservationDocumentation.TASKS_SCHEDULED_EXECUTION.observation( + null, DEFAULT_CONVENTION, + () -> context, this.observationRegistrySupplier.get()); + observation.observe(() -> runInternal(context)); + } + + private void runInternal(ScheduledTaskObservationContext context) { try { ReflectionUtils.makeAccessible(this.method); this.method.invoke(this.target); + context.setComplete(true); } catch (InvocationTargetException ex) { ReflectionUtils.rethrowRuntimeException(ex.getTargetException()); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationContext.java b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationContext.java new file mode 100644 index 000000000000..a7f7e96ca0f4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationContext.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.support; + +import java.lang.reflect.Method; + +import io.micrometer.observation.Observation; + +import org.springframework.util.ClassUtils; + +/** + * Context that holds information for observation metadata collection during the + * {@link ScheduledTaskObservationDocumentation#TASKS_SCHEDULED_EXECUTION execution of scheduled tasks}. + * + * @author Brian Clozel + * @since 6.1 + */ +public class ScheduledTaskObservationContext extends Observation.Context { + + private final Class targetClass; + + private final Method method; + + private boolean complete; + + + /** + * Create a new observation context for a task, given the target object + * and the method to be called. + * @param target the target object that is called for task execution + * @param method the method that is called for task execution + */ + public ScheduledTaskObservationContext(Object target, Method method) { + this.targetClass = ClassUtils.getUserClass(target); + this.method = method; + } + + + /** + * Return the type of the target object. + */ + public Class getTargetClass() { + return this.targetClass; + } + + /** + * Return the method that is called for task execution. + */ + public Method getMethod() { + return this.method; + } + + /** + * Return whether the task execution is complete. + *

If an observation has ended and the task is not complete, this means + * that an {@link #getError() error} was raised or that the task execution got cancelled + * during its execution. + */ + public boolean isComplete() { + return this.complete; + } + + /** + * Set whether the task execution has completed. + */ + public void setComplete(boolean complete) { + this.complete = complete; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationConvention.java b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationConvention.java new file mode 100644 index 000000000000..23f53bf60967 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationConvention.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.support; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * Interface for an {@link ObservationConvention} for + * {@link ScheduledTaskObservationDocumentation#TASKS_SCHEDULED_EXECUTION scheduled task executions}. + * + * @author Brian Clozel + * @since 6.1 + */ +public interface ScheduledTaskObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof ScheduledTaskObservationContext; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationDocumentation.java b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationDocumentation.java new file mode 100644 index 000000000000..4d80fdab7d3e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledTaskObservationDocumentation.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.support; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * Documented {@link io.micrometer.common.KeyValue KeyValues} for the observations on + * executions of {@link org.springframework.scheduling.annotation.Scheduled scheduled tasks} + * + *

This class is used by automated tools to document KeyValues attached to the + * {@code @Scheduled} observations. + * + * @author Brian Clozel + * @since 6.1 + */ +public enum ScheduledTaskObservationDocumentation implements ObservationDocumentation { + + /** + * Observations on executions of {@link org.springframework.scheduling.annotation.Scheduled} tasks. + */ + TASKS_SCHEDULED_EXECUTION { + @Override + public Class> getDefaultConvention() { + return DefaultScheduledTaskObservationConvention.class; + } + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + @Override + public KeyName[] getHighCardinalityKeyNames() { + return new KeyName[] {}; + } + }; + + + public enum LowCardinalityKeyNames implements KeyName { + + /** + * Name of the method that is executed for the scheduled task. + */ + CODE_FUNCTION { + @Override + public String asString() { + return "code.function"; + } + }, + + /** + * {@link Class#getCanonicalName() Canonical name} of the target type that owns the scheduled method. + */ + CODE_NAMESPACE { + @Override + public String asString() { + return "code.namespace"; + } + }, + + /** + * Name of the exception thrown during task execution, or {@value KeyValue#NONE_VALUE} if no exception was thrown. + */ + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + /** + * Outcome of the scheduled task execution. + */ + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + } + + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java b/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java index 740f905c0acc..52088fb41dde 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,7 @@ public SimpleTriggerContext(@Nullable Date lastScheduledExecutionTime, @Nullable @Nullable private static Instant toInstant(@Nullable Date date) { - return date != null ? date.toInstant() : null; + return (date != null ? date.toInstant() : null); } /** diff --git a/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java index ba2e2c4e906d..f4068e09cbb0 100644 --- a/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java +++ b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -38,6 +39,7 @@ class ScriptingDefaultsParser implements BeanDefinitionParser { @Override + @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { BeanDefinition bd = LangNamespaceUtils.registerScriptFactoryPostProcessorIfNecessary(parserContext.getRegistry()); diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java index e181bc00205a..6d8b6c5e2e09 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -305,6 +305,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str } @Override + @Nullable public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { // We only apply special treatment to ScriptFactory implementations here. if (!ScriptFactory.class.isAssignableFrom(beanClass)) { diff --git a/spring-context/src/main/java/org/springframework/stereotype/Component.java b/spring-context/src/main/java/org/springframework/stereotype/Component.java index a6e09edc6e79..abf53ecd6b95 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Component.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Component.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,16 +23,40 @@ import java.lang.annotation.Target; /** - * Indicates that an annotated class is a "component". - * Such classes are considered as candidates for auto-detection + * Indicates that the annotated class is a component. + * + *

Such classes are considered as candidates for auto-detection * when using annotation-based configuration and classpath scanning. * + *

A component may optionally specify a logical component name via the + * {@link #value value} attribute of this annotation. + * *

Other class-level annotations may be considered as identifying - * a component as well, typically a special kind of component: - * e.g. the {@link Repository @Repository} annotation or AspectJ's - * {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation. + * a component as well, typically a special kind of component — + * for example, the {@link Repository @Repository} annotation or AspectJ's + * {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation. Note, however, + * that the {@code @Aspect} annotation does not automatically make a class + * eligible for classpath scanning. + * + *

Any annotation meta-annotated with {@code @Component} is considered a + * stereotype annotation which makes the annotated class eligible for + * classpath scanning. For example, {@link Service @Service}, + * {@link Controller @Controller}, and {@link Repository @Repository} are + * stereotype annotations. Stereotype annotations may also support configuration + * of a logical component name by overriding the {@link #value} attribute of this + * annotation via {@link org.springframework.core.annotation.AliasFor @AliasFor}. + * + *

As of Spring Framework 6.1, support for configuring the name of a stereotype + * component by convention (i.e., via a {@code String value()} attribute without + * {@code @AliasFor}) is deprecated and will be removed in a future version of the + * framework. Consequently, custom stereotype annotations must use {@code @AliasFor} + * to declare an explicit alias for this annotation's {@link #value} attribute. + * See the source code declaration of {@link Repository#value()} and + * {@link org.springframework.web.bind.annotation.ControllerAdvice#name() + * ControllerAdvice.name()} for concrete examples. * * @author Mark Fisher + * @author Sam Brannen * @since 2.5 * @see Repository * @see Service @@ -47,7 +71,7 @@ /** * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. + * to be turned into a Spring bean name in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) */ String value() default ""; diff --git a/spring-context/src/main/java/org/springframework/stereotype/Controller.java b/spring-context/src/main/java/org/springframework/stereotype/Controller.java index 6629feea4b3e..a991c31142ee 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Controller.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Controller.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,9 +46,7 @@ public @interface Controller { /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) + * Alias for {@link Component#value}. */ @AliasFor(annotation = Component.class) String value() default ""; diff --git a/spring-context/src/main/java/org/springframework/stereotype/Repository.java b/spring-context/src/main/java/org/springframework/stereotype/Repository.java index 22e54ab8cd0f..5a964bbd063f 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Repository.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Repository.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,9 +43,8 @@ * to its role in the overall application architecture for the purpose of tooling, * aspects, etc. * - *

As of Spring 2.5, this annotation also serves as a specialization of - * {@link Component @Component}, allowing for implementation classes to be autodetected - * through classpath scanning. + *

This annotation also serves as a specialization of {@link Component @Component}, + * allowing for implementation classes to be autodetected through classpath scanning. * * @author Rod Johnson * @author Juergen Hoeller @@ -62,9 +61,7 @@ public @interface Repository { /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) + * Alias for {@link Component#value}. */ @AliasFor(annotation = Component.class) String value() default ""; diff --git a/spring-context/src/main/java/org/springframework/stereotype/Service.java b/spring-context/src/main/java/org/springframework/stereotype/Service.java index 5c714540bbca..92a9a70601f8 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Service.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Service.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,9 +48,7 @@ public @interface Service { /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) + * Alias for {@link Component#value}. */ @AliasFor(annotation = Component.class) String value() default ""; diff --git a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java index 0d2b6afaeff0..25e3d4d7b052 100644 --- a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java +++ b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ public ConcurrentModel() { } /** - * Construct a new {@code ModelMap} containing the supplied attribute + * Construct a new {@code ConcurrentModel} containing the supplied attribute * under the supplied name. * @see #addAttribute(String, Object) */ @@ -55,8 +55,8 @@ public ConcurrentModel(String attributeName, Object attributeValue) { } /** - * Construct a new {@code ModelMap} containing the supplied attribute. - * Uses attribute name generation to generate the key for the supplied model + * Construct a new {@code ConcurrentModel} containing the supplied attribute. + *

Uses attribute name generation to generate the key for the supplied model * object. * @see #addAttribute(Object) */ diff --git a/spring-context/src/main/java/org/springframework/ui/package-info.java b/spring-context/src/main/java/org/springframework/ui/package-info.java index 988880e3b9a8..96269d532a03 100644 --- a/spring-context/src/main/java/org/springframework/ui/package-info.java +++ b/spring-context/src/main/java/org/springframework/ui/package-info.java @@ -1,6 +1,6 @@ /** * Generic support for UI layer concepts. - * Provides a generic ModelMap for model holding. + *

Provides generic {@code Model} and {@code ModelMap} holders for model attributes. */ @NonNullApi @NonNullFields diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java index b9e5617a53c6..460d860f85a5 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,7 +82,7 @@ protected void doSetNestedPath(@Nullable String nestedPath) { nestedPath = ""; } nestedPath = canonicalFieldName(nestedPath); - if (nestedPath.length() > 0 && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { + if (!nestedPath.isEmpty() && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { nestedPath += NESTED_PATH_SEPARATOR; } this.nestedPath = nestedPath; @@ -113,90 +113,6 @@ protected String canonicalFieldName(String field) { return field; } - - @Override - public void reject(String errorCode) { - reject(errorCode, null, null); - } - - @Override - public void reject(String errorCode, String defaultMessage) { - reject(errorCode, null, defaultMessage); - } - - @Override - public void rejectValue(@Nullable String field, String errorCode) { - rejectValue(field, errorCode, null, null); - } - - @Override - public void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { - rejectValue(field, errorCode, null, defaultMessage); - } - - - @Override - public boolean hasErrors() { - return !getAllErrors().isEmpty(); - } - - @Override - public int getErrorCount() { - return getAllErrors().size(); - } - - @Override - public List getAllErrors() { - List result = new ArrayList<>(); - result.addAll(getGlobalErrors()); - result.addAll(getFieldErrors()); - return Collections.unmodifiableList(result); - } - - @Override - public boolean hasGlobalErrors() { - return (getGlobalErrorCount() > 0); - } - - @Override - public int getGlobalErrorCount() { - return getGlobalErrors().size(); - } - - @Override - @Nullable - public ObjectError getGlobalError() { - List globalErrors = getGlobalErrors(); - return (!globalErrors.isEmpty() ? globalErrors.get(0) : null); - } - - @Override - public boolean hasFieldErrors() { - return (getFieldErrorCount() > 0); - } - - @Override - public int getFieldErrorCount() { - return getFieldErrors().size(); - } - - @Override - @Nullable - public FieldError getFieldError() { - List fieldErrors = getFieldErrors(); - return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); - } - - @Override - public boolean hasFieldErrors(String field) { - return (getFieldErrorCount(field) > 0); - } - - @Override - public int getFieldErrorCount(String field) { - return getFieldErrors(field).size(); - } - @Override public List getFieldErrors(String field) { List fieldErrors = getFieldErrors(); @@ -210,20 +126,6 @@ public List getFieldErrors(String field) { return Collections.unmodifiableList(result); } - @Override - @Nullable - public FieldError getFieldError(String field) { - List fieldErrors = getFieldErrors(field); - return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); - } - - @Override - @Nullable - public Class getFieldType(String field) { - Object value = getFieldValue(field); - return (value != null ? value.getClass() : null); - } - /** * Check whether the given FieldError matches the given field. * @param field the field that we are looking up FieldErrors for diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java index 15ef3ae4667f..61675d953e17 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java @@ -70,6 +70,7 @@ public void initConversion(ConversionService conversionService) { * @see #getPropertyAccessor() */ @Override + @Nullable public PropertyEditorRegistry getPropertyEditorRegistry() { return (getTarget() != null ? getPropertyAccessor() : null); } @@ -109,6 +110,7 @@ protected Object getActualFieldValue(String field) { * @see #getCustomEditor */ @Override + @Nullable protected Object formatFieldValue(String field, @Nullable Object value) { String fixedField = fixedField(field); // Try custom editor... diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index c2e5ecfa3b07..c7af62b50117 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,41 @@ package org.springframework.validation; import java.beans.PropertyEditor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; import org.springframework.beans.ConfigurablePropertyAccessor; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessException; import org.springframework.beans.PropertyAccessorUtils; import org.springframework.beans.PropertyBatchUpdateException; +import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValues; import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.format.Formatter; @@ -49,10 +61,11 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.ValidationAnnotationUtils; /** - * Binder that allows for setting property values on a target object, including - * support for validation and binding result analysis. + * Binder that allows applying property values to a target object via constructor + * and setter injection, and also supports validation and binding result analysis. * *

The binding process can be customized by specifying allowed field patterns, * required fields, custom editors, etc. @@ -104,6 +117,7 @@ * @see #registerCustomEditor * @see #setMessageCodesResolver * @see #setBindingErrorProcessor + * @see #construct * @see #bind * @see #getBindingResult * @see DefaultMessageCodesResolver @@ -125,7 +139,10 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { protected static final Log logger = LogFactory.getLog(DataBinder.class); @Nullable - private final Object target; + private Object target; + + @Nullable + ResolvableType targetType; private final String objectName; @@ -135,7 +152,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { private boolean directFieldAccess = false; @Nullable - private SimpleTypeConverter typeConverter; + private ExtendedTypeConverter typeConverter; + + private boolean declarativeBinding = false; private boolean ignoreUnknownFields = true; @@ -154,6 +173,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @Nullable private String[] requiredFields; + @Nullable + private NameResolver nameResolver; + @Nullable private ConversionService conversionService; @@ -164,6 +186,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { private final List validators = new ArrayList<>(); + @Nullable + private Predicate excludedValidators; + /** * Create a new DataBinder instance, with default object name. @@ -189,6 +214,8 @@ public DataBinder(@Nullable Object target, String objectName) { /** * Return the wrapped target object. + *

If the target object is {@code null} and {@link #getTargetType()} is set, + * then {@link #construct(ValueResolver)} may be called to create the target. */ @Nullable public Object getTarget() { @@ -202,6 +229,27 @@ public String getObjectName() { return this.objectName; } + /** + * Set the type for the target object. When the target is {@code null}, + * setting the targetType allows using {@link #construct} to create the target. + * @param targetType the type of the target object + * @since 6.1 + * @see #construct + */ + public void setTargetType(ResolvableType targetType) { + Assert.state(this.target == null, "targetType is used to for target creation but target is already set"); + this.targetType = targetType; + } + + /** + * Return the {@link #setTargetType configured} type for the target object. + * @since 6.1 + */ + @Nullable + public ResolvableType getTargetType() { + return this.targetType; + } + /** * Set whether this binder should attempt to "auto-grow" a nested path that contains a null value. *

If "true", a null path location will be populated with a default object value and traversed @@ -209,6 +257,8 @@ public String getObjectName() { * when accessing an out-of-bounds index. *

Default is "true" on a standard DataBinder. Note that since Spring 4.1 this feature is supported * for bean property access (DataBinder's default mode) and field access. + *

Used for setter/field injection via {@link #bind(PropertyValues)}, and not + * applicable to constructor binding via {@link #construct}. * @see #initBeanPropertyAccess() * @see org.springframework.beans.BeanWrapper#setAutoGrowNestedPaths */ @@ -229,6 +279,8 @@ public boolean isAutoGrowNestedPaths() { * Specify the limit for array and collection auto-growing. *

Default is 256, preventing OutOfMemoryErrors in case of large indexes. * Raise this limit if your auto-growing needs are unusually high. + *

Used for setter/field injection via {@link #bind(PropertyValues)}, and not + * applicable to constructor binding via {@link #construct}. * @see #initBeanPropertyAccess() * @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit */ @@ -331,7 +383,7 @@ protected ConfigurablePropertyAccessor getPropertyAccessor() { */ protected SimpleTypeConverter getSimpleTypeConverter() { if (this.typeConverter == null) { - this.typeConverter = new SimpleTypeConverter(); + this.typeConverter = new ExtendedTypeConverter(); if (this.conversionService != null) { this.typeConverter.setConversionService(this.conversionService); } @@ -376,6 +428,28 @@ public BindingResult getBindingResult() { return getInternalBindingResult(); } + /** + * Set whether to bind only fields explicitly intended for binding including: + *

    + *
  • Constructor binding via {@link #construct}. + *
  • Property binding with configured + * {@link #setAllowedFields(String...) allowedFields}. + *
+ *

Default is "false". Turn this on to limit binding to constructor + * parameters and allowed fields. + * @since 6.1 + */ + public void setDeclarativeBinding(boolean declarativeBinding) { + this.declarativeBinding = declarativeBinding; + } + + /** + * Return whether to bind only fields intended for binding. + * @since 6.1 + */ + public boolean isDeclarativeBinding() { + return this.declarativeBinding; + } /** * Set whether to ignore unknown fields, that is, whether to ignore bind @@ -385,6 +459,9 @@ public BindingResult getBindingResult() { *

Note that this setting only applies to binding operations * on this DataBinder, not to retrieving values via its * {@link #getBindingResult() BindingResult}. + *

Used for binding to fields with {@link #bind(PropertyValues)}, + * and not applicable to constructor binding via {@link #construct} + * which uses only the values it needs. * @see #bind */ public void setIgnoreUnknownFields(boolean ignoreUnknownFields) { @@ -407,6 +484,9 @@ public boolean isIgnoreUnknownFields() { *

Note that this setting only applies to binding operations * on this DataBinder, not to retrieving values via its * {@link #getBindingResult() BindingResult}. + *

Used for binding to fields with {@link #bind(PropertyValues)}, and not + * applicable to constructor binding via {@link #construct}, + * which uses only the values it needs. * @see #bind */ public void setIgnoreInvalidFields(boolean ignoreInvalidFields) { @@ -435,6 +515,9 @@ public boolean isIgnoreInvalidFields() { *

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

Alternatively, specify a list of disallowed field patterns. + *

Used for binding to fields with {@link #bind(PropertyValues)}, and not + * applicable to constructor binding via {@link #construct}, + * which uses only the values it needs. * @param allowedFields array of allowed field patterns * @see #setDisallowedFields * @see #isAllowed(String) @@ -471,6 +554,9 @@ public String[] getAllowedFields() { *

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

Alternatively, specify a list of allowed field patterns. + *

Used for binding to fields with {@link #bind(PropertyValues)}, and not + * applicable to constructor binding via {@link #construct}, + * which uses only the values it needs. * @param disallowedFields array of disallowed field patterns * @see #setAllowedFields * @see #isAllowed(String) @@ -504,6 +590,9 @@ public String[] getDisallowedFields() { * incoming property values, a corresponding "missing field" error * will be created, with error code "required" (by the default * binding error processor). + *

Used for binding to fields with {@link #bind(PropertyValues)}, and not + * applicable to constructor binding via {@link #construct}, + * which uses only the values it needs. * @param requiredFields array of field names * @see #setBindingErrorProcessor * @see DefaultBindingErrorProcessor#MISSING_FIELD_ERROR_CODE @@ -525,6 +614,28 @@ public String[] getRequiredFields() { return this.requiredFields; } + /** + * Configure a resolver to determine the name of the value to bind to a + * constructor parameter in {@link #construct}. + *

If not configured, or if the name cannot be resolved, by default + * {@link org.springframework.core.DefaultParameterNameDiscoverer} is used. + * @param nameResolver the resolver to use + * @since 6.1 + */ + public void setNameResolver(NameResolver nameResolver) { + this.nameResolver = nameResolver; + } + + /** + * Return the {@link #setNameResolver configured} name resolver for + * constructor parameters. + * @since 6.1 + */ + @Nullable + public NameResolver getNameResolver() { + return this.nameResolver; + } + /** * Set the strategy to use for resolving errors into message codes. * Applies the given strategy to the underlying errors holder. @@ -580,6 +691,14 @@ private void assertValidators(Validator... validators) { } } + /** + * Configure a predicate to exclude validators. + * @since 6.1 + */ + public void setExcludedValidators(Predicate predicate) { + this.excludedValidators = predicate; + } + /** * Add Validators to apply after each binding step. * @see #setValidator(Validator) @@ -616,6 +735,18 @@ public List getValidators() { return Collections.unmodifiableList(this.validators); } + /** + * Return the Validators to apply after data binding. This includes the + * configured {@link #getValidators() validators} filtered by the + * {@link #setExcludedValidators(Predicate) exclude predicate}. + * @since 6.1 + */ + public List getValidatorsToApply() { + return (this.excludedValidators != null ? + this.validators.stream().filter(validator -> !this.excludedValidators.test(validator)).toList() : + Collections.unmodifiableList(this.validators)); + } + //--------------------------------------------------------------------- // Implementation of PropertyEditorRegistry/TypeConverter interface @@ -746,6 +877,194 @@ public T convertIfNecessary(@Nullable Object value, @Nullable Class requi } + /** + * Create the target with constructor injection of values. It is expected that + * {@link #setTargetType(ResolvableType)} was previously called and that + * {@link #getTarget()} is {@code null}. + *

Uses a public, no-arg constructor if available in the target object type, + * also supporting a "primary constructor" approach for data classes as follows: + * It understands the JavaBeans {@code ConstructorProperties} annotation as + * well as runtime-retained parameter names in the bytecode, associating + * input values with constructor arguments by name. If no such constructor is + * found, the default constructor will be used (even if not public), assuming + * subsequent bean property bindings through setter methods. + *

After the call, use {@link #getBindingResult()} to check for failures + * to bind to, and/or validate constructor arguments. If there are no errors, + * the target is set, and {@link #doBind(MutablePropertyValues)} can be used + * for further initialization via setters. + * @param valueResolver to resolve constructor argument values with + * @throws BeanInstantiationException in case of constructor failure + * @since 6.1 + */ + public void construct(ValueResolver valueResolver) { + Assert.state(this.target == null, "Target instance already available"); + Assert.state(this.targetType != null, "Target type not set"); + + this.target = createObject(this.targetType, "", valueResolver); + + if (!getBindingResult().hasErrors()) { + this.bindingResult = null; + if (this.typeConverter != null) { + this.typeConverter.registerCustomEditors(getPropertyAccessor()); + } + } + } + + @Nullable + private Object createObject(ResolvableType objectType, String nestedPath, ValueResolver valueResolver) { + Class clazz = objectType.resolve(); + boolean isOptional = (clazz == Optional.class); + clazz = (isOptional ? objectType.resolveGeneric(0) : clazz); + if (clazz == null) { + throw new IllegalStateException( + "Insufficient type information to create instance of " + objectType); + } + + Object result = null; + Constructor ctor = BeanUtils.getResolvableConstructor(clazz); + + if (ctor.getParameterCount() == 0) { + // A single default constructor -> clearly a standard JavaBeans arrangement. + result = BeanUtils.instantiateClass(ctor); + } + else { + // A single data class constructor -> resolve constructor arguments from request parameters. + String[] paramNames = BeanUtils.getParameterNames(ctor); + Class[] paramTypes = ctor.getParameterTypes(); + Object[] args = new Object[paramTypes.length]; + Set failedParamNames = new HashSet<>(4); + + for (int i = 0; i < paramNames.length; i++) { + MethodParameter param = MethodParameter.forFieldAwareConstructor(ctor, i, paramNames[i]); + String lookupName = null; + if (this.nameResolver != null) { + lookupName = this.nameResolver.resolveName(param); + } + if (lookupName == null) { + lookupName = paramNames[i]; + } + + String paramPath = nestedPath + lookupName; + Class paramType = paramTypes[i]; + Object value = valueResolver.resolveValue(paramPath, paramType); + + if (value == null && shouldConstructArgument(param) && hasValuesFor(paramPath, valueResolver)) { + ResolvableType type = ResolvableType.forMethodParameter(param); + args[i] = createObject(type, paramPath + ".", valueResolver); + } + else { + try { + if (value == null && (param.isOptional() || getBindingResult().hasErrors())) { + args[i] = (param.getParameterType() == Optional.class ? Optional.empty() : null); + } + else { + args[i] = convertIfNecessary(value, paramType, param); + } + } + catch (TypeMismatchException ex) { + ex.initPropertyName(paramPath); + args[i] = null; + failedParamNames.add(paramPath); + getBindingResult().recordFieldValue(paramPath, paramType, value); + getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult()); + } + } + } + + if (getBindingResult().hasErrors()) { + for (int i = 0; i < paramNames.length; i++) { + String paramPath = nestedPath + paramNames[i]; + if (!failedParamNames.contains(paramPath)) { + Object value = args[i]; + getBindingResult().recordFieldValue(paramPath, paramTypes[i], value); + validateConstructorArgument(ctor.getDeclaringClass(), nestedPath, paramNames[i], value); + } + } + if (!(objectType.getSource() instanceof MethodParameter param && param.isOptional())) { + try { + result = BeanUtils.instantiateClass(ctor, args); + } + catch (BeanInstantiationException ex) { + // swallow and proceed without target instance + } + } + } + else { + try { + result = BeanUtils.instantiateClass(ctor, args); + } + catch (BeanInstantiationException ex) { + if (KotlinDetector.isKotlinType(clazz) && ex.getCause() instanceof NullPointerException cause) { + ObjectError error = new ObjectError(ctor.getName(), cause.getMessage()); + getBindingResult().addError(error); + } + else { + throw ex; + } + } + } + } + + return (isOptional && !nestedPath.isEmpty() ? Optional.ofNullable(result) : result); + } + + /** + * Whether to instantiate the constructor argument of the given type, + * matching its own constructor arguments to bind values. + *

By default, simple value types, maps, collections, and arrays are + * excluded from nested constructor binding initialization. + * @since 6.1.2 + */ + protected boolean shouldConstructArgument(MethodParameter param) { + Class type = param.nestedIfOptional().getNestedParameterType(); + return !(BeanUtils.isSimpleValueType(type) || + Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) || type.isArray() || + type.getPackageName().startsWith("java.")); + } + + private boolean hasValuesFor(String paramPath, ValueResolver resolver) { + for (String name : resolver.getNames()) { + if (name.startsWith(paramPath + ".")) { + return true; + } + } + return false; + } + + private void validateConstructorArgument( + Class constructorClass, String nestedPath, String name, @Nullable Object value) { + + Object[] hints = null; + if (this.targetType != null && this.targetType.getSource() instanceof MethodParameter parameter) { + for (Annotation ann : parameter.getParameterAnnotations()) { + hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + break; + } + } + } + if (hints == null) { + return; + } + for (Validator validator : getValidatorsToApply()) { + if (validator instanceof SmartValidator smartValidator) { + boolean isNested = !nestedPath.isEmpty(); + if (isNested) { + getBindingResult().pushNestedPath(nestedPath.substring(0, nestedPath.length() - 1)); + } + try { + smartValidator.validateValue(constructorClass, name, value, getBindingResult(), hints); + } + catch (IllegalArgumentException ex) { + // No corresponding field on the target class... + } + if (isNested) { + getBindingResult().popNestedPath(); + } + } + } + } + /** * Bind the given property values to this binder's target. *

This call can create field errors, representing basic binding @@ -760,11 +1079,24 @@ public T convertIfNecessary(@Nullable Object value, @Nullable Class requi * @see #doBind(org.springframework.beans.MutablePropertyValues) */ public void bind(PropertyValues pvs) { + if (shouldNotBindPropertyValues()) { + return; + } MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues mutablePropertyValues ? mutablePropertyValues : new MutablePropertyValues(pvs)); doBind(mpvs); } + /** + * Whether to not bind parameters to properties. Returns "true" if + * {@link #isDeclarativeBinding()} is on, and + * {@link #setAllowedFields(String...) allowedFields} are not configured. + * @since 6.1 + */ + protected boolean shouldNotBindPropertyValues() { + return (isDeclarativeBinding() && ObjectUtils.isEmpty(this.allowedFields)); + } + /** * Actual implementation of the binding process, working with the * passed-in MutablePropertyValues instance. @@ -906,7 +1238,7 @@ public void validate() { Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); // Call each validator with the same binding result - for (Validator validator : getValidators()) { + for (Validator validator : getValidatorsToApply()) { validator.validate(target, bindingResult); } } @@ -924,7 +1256,7 @@ public void validate(Object... validationHints) { Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); // Call each validator with the same binding result - for (Validator validator : getValidators()) { + for (Validator validator : getValidatorsToApply()) { if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) { smartValidator.validate(target, bindingResult, validationHints); } @@ -948,4 +1280,59 @@ else if (validator != null) { return getBindingResult().getModel(); } + + /** + * Strategy to determine the name of the value to bind to a method parameter. + * Supported on constructor parameters with {@link #construct constructor binding} + * which performs lookups via {@link ValueResolver#resolveValue}. + */ + public interface NameResolver { + + /** + * Return the name to use for the given method parameter, or {@code null} + * if unresolved. For constructor parameters, the name is determined via + * {@link org.springframework.core.DefaultParameterNameDiscoverer} if unresolved. + */ + @Nullable + String resolveName(MethodParameter parameter); + } + + + /** + * Strategy for {@link #construct constructor binding} to look up the values + * to bind to a given constructor parameter. + */ + public interface ValueResolver { + + /** + * Resolve the value for the given name and target parameter type. + * @param name the name to use for the lookup, possibly a nested path + * for constructor parameters on nested objects + * @param type the target type, based on the constructor parameter type + * @return the resolved value, possibly {@code null} if none found + */ + @Nullable + Object resolveValue(String name, Class type); + + /** + * Return the names of all property values. + * @since 6.1.2 + */ + Set getNames(); + + } + + + /** + * {@link SimpleTypeConverter} that is also {@link PropertyEditorRegistrar}. + */ + private static class ExtendedTypeConverter + extends SimpleTypeConverter implements PropertyEditorRegistrar { + + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + copyCustomEditorsTo(registry, null); + } + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java index 0b69e3f072d2..7d5bf02f9802 100644 --- a/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java @@ -66,7 +66,7 @@ public void processMissingFieldError(String missingField, BindingResult bindingR @Override public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) { - // Create field error with the exceptions's code, e.g. "typeMismatch". + // Create field error with the code of the exception, e.g. "typeMismatch". String field = ex.getPropertyName(); Assert.state(field != null, "No field in exception"); String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field); diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java index 18a7bc1910af..a44bbd3ad5c7 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -17,6 +17,9 @@ package org.springframework.validation; import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; import org.springframework.beans.PropertyAccessor; import org.springframework.lang.Nullable; @@ -38,7 +41,7 @@ * @author Juergen Hoeller * @see Validator * @see ValidationUtils - * @see BindException + * @see SimpleErrors * @see BindingResult */ public interface Errors { @@ -63,12 +66,16 @@ public interface Errors { * subtrees. Reject calls prepend the given path to the field names. *

For example, an address validator could validate the subobject * "address" of a customer object. + *

The default implementation throws {@code UnsupportedOperationException} + * since not all {@code Errors} implementations support nested paths. * @param nestedPath nested path within this object, * e.g. "address" (defaults to "", {@code null} is also acceptable). * Can end with a dot: both "address" and "address." are valid. * @see #getNestedPath() */ - void setNestedPath(String nestedPath); + default void setNestedPath(String nestedPath) { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support nested paths"); + } /** * Return the current nested path of this {@link Errors} object. @@ -76,7 +83,9 @@ public interface Errors { * building of concatenated paths. Default is an empty String. * @see #setNestedPath(String) */ - String getNestedPath(); + default String getNestedPath() { + return ""; + } /** * Push the given sub path onto the nested path stack. @@ -87,17 +96,23 @@ public interface Errors { * for subobjects without having to worry about a temporary path holder. *

For example: current path "spouse.", pushNestedPath("child") → * result path "spouse.child."; popNestedPath() → "spouse." again. + *

The default implementation throws {@code UnsupportedOperationException} + * since not all {@code Errors} implementations support nested paths. * @param subPath the sub path to push onto the nested path stack * @see #popNestedPath() */ - void pushNestedPath(String subPath); + default void pushNestedPath(String subPath) { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support nested paths"); + } /** * Pop the former nested path from the nested path stack. * @throws IllegalStateException if there is no former nested path on the stack * @see #pushNestedPath(String) */ - void popNestedPath() throws IllegalStateException; + default void popNestedPath() throws IllegalStateException { + throw new IllegalStateException("Cannot pop nested path: no nested path on stack"); + } /** * Register a global error for the entire target object, @@ -105,7 +120,9 @@ public interface Errors { * @param errorCode error code, interpretable as a message key * @see #reject(String, Object[], String) */ - void reject(String errorCode); + default void reject(String errorCode) { + reject(errorCode, null, null); + } /** * Register a global error for the entire target object, @@ -114,7 +131,9 @@ public interface Errors { * @param defaultMessage fallback default message * @see #reject(String, Object[], String) */ - void reject(String errorCode, String defaultMessage); + default void reject(String errorCode, String defaultMessage) { + reject(errorCode, null, defaultMessage); + } /** * Register a global error for the entire target object, @@ -139,7 +158,9 @@ public interface Errors { * @param errorCode error code, interpretable as a message key * @see #rejectValue(String, String, Object[], String) */ - void rejectValue(@Nullable String field, String errorCode); + default void rejectValue(@Nullable String field, String errorCode) { + rejectValue(field, errorCode, null, null); + } /** * Register a field error for the specified field of the current object @@ -154,7 +175,9 @@ public interface Errors { * @param defaultMessage fallback default message * @see #rejectValue(String, String, Object[], String) */ - void rejectValue(@Nullable String field, String errorCode, String defaultMessage); + default void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { + rejectValue(field, errorCode, null, defaultMessage); + } /** * Register a field error for the specified field of the current object @@ -183,24 +206,46 @@ void rejectValue(@Nullable String field, String errorCode, *

Note that the passed-in {@code Errors} instance is supposed * to refer to the same target object, or at least contain compatible errors * that apply to the target object of this {@code Errors} instance. + *

The default implementation throws {@code UnsupportedOperationException} + * since not all {@code Errors} implementations support {@code #addAllErrors}. * @param errors the {@code Errors} instance to merge in * @see #getAllErrors() */ - void addAllErrors(Errors errors); + default void addAllErrors(Errors errors) { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support addAllErrors"); + } + + /** + * Throw the mapped exception with a message summarizing the recorded errors. + * @param messageToException a function mapping the message to the exception, + * e.g. {@code IllegalArgumentException::new} or {@code IllegalStateException::new} + * @param the exception type to be thrown + * @since 6.1 + * @see #toString() + */ + default void failOnError(Function messageToException) throws T { + if (hasErrors()) { + throw messageToException.apply(toString()); + } + } /** * Determine if there were any errors. * @see #hasGlobalErrors() * @see #hasFieldErrors() */ - boolean hasErrors(); + default boolean hasErrors() { + return (!getGlobalErrors().isEmpty() || !getFieldErrors().isEmpty()); + } /** * Determine the total number of errors. * @see #getGlobalErrorCount() * @see #getFieldErrorCount() */ - int getErrorCount(); + default int getErrorCount() { + return (getGlobalErrors().size() + getFieldErrors().size()); + } /** * Get all errors, both global and field ones. @@ -208,19 +253,25 @@ void rejectValue(@Nullable String field, String errorCode, * @see #getGlobalErrors() * @see #getFieldErrors() */ - List getAllErrors(); + default List getAllErrors() { + return Stream.concat(getGlobalErrors().stream(), getFieldErrors().stream()).toList(); + } /** * Determine if there were any global errors. * @see #hasFieldErrors() */ - boolean hasGlobalErrors(); + default boolean hasGlobalErrors() { + return !getGlobalErrors().isEmpty(); + } /** * Determine the number of global errors. * @see #getFieldErrorCount() */ - int getGlobalErrorCount(); + default int getGlobalErrorCount() { + return getGlobalErrors().size(); + } /** * Get all global errors. @@ -235,19 +286,25 @@ void rejectValue(@Nullable String field, String errorCode, * @see #getFieldError() */ @Nullable - ObjectError getGlobalError(); + default ObjectError getGlobalError() { + return getGlobalErrors().stream().findFirst().orElse(null); + } /** * Determine if there were any errors associated with a field. * @see #hasGlobalErrors() */ - boolean hasFieldErrors(); + default boolean hasFieldErrors() { + return !getFieldErrors().isEmpty(); + } /** * Determine the number of errors associated with a field. * @see #getGlobalErrorCount() */ - int getFieldErrorCount(); + default int getFieldErrorCount() { + return getFieldErrors().size(); + } /** * Get all errors associated with a field. @@ -262,21 +319,27 @@ void rejectValue(@Nullable String field, String errorCode, * @see #getGlobalError() */ @Nullable - FieldError getFieldError(); + default FieldError getFieldError() { + return getFieldErrors().stream().findFirst().orElse(null); + } /** * Determine if there were any errors associated with the given field. * @param field the field name * @see #hasFieldErrors() */ - boolean hasFieldErrors(String field); + default boolean hasFieldErrors(String field) { + return (getFieldError(field) != null); + } /** * Determine the number of errors associated with the given field. * @param field the field name * @see #getFieldErrorCount() */ - int getFieldErrorCount(String field); + default int getFieldErrorCount(String field) { + return getFieldErrors(field).size(); + } /** * Get all errors associated with the given field. @@ -286,7 +349,9 @@ void rejectValue(@Nullable String field, String errorCode, * @return a List of {@link FieldError} instances * @see #getFieldErrors() */ - List getFieldErrors(String field); + default List getFieldErrors(String field) { + return getFieldErrors().stream().filter(error -> field.equals(error.getField())).toList(); + } /** * Get the first error associated with the given field, if any. @@ -295,7 +360,9 @@ void rejectValue(@Nullable String field, String errorCode, * @see #getFieldError() */ @Nullable - FieldError getFieldError(String field); + default FieldError getFieldError(String field) { + return getFieldErrors().stream().filter(error -> field.equals(error.getField())).findFirst().orElse(null); + } /** * Return the current value of the given field, either the current @@ -319,6 +386,15 @@ void rejectValue(@Nullable String field, String errorCode, * @see #getFieldValue(String) */ @Nullable - Class getFieldType(String field); + default Class getFieldType(String field) { + return Optional.ofNullable(getFieldValue(field)).map(Object::getClass).orElse(null); + } + + /** + * Return a summary of the recorded errors, + * e.g. for inclusion in an exception message. + * @see #failOnError(Function) + */ + String toString(); } diff --git a/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java b/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java new file mode 100644 index 000000000000..6854f26235ea --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java @@ -0,0 +1,190 @@ +/* + * 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.validation; + +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A simple implementation of the {@link Errors} interface, managing global + * errors and field errors for a top-level target object. Flexibly retrieves + * field values through bean property getter methods, and automatically + * falls back to raw field access if necessary. + * + *

Note that this {@link Errors} implementation comes without support for + * nested paths. It is exclusively designed for the validation of individual + * top-level objects, not aggregating errors from multiple sources. + * If this is insufficient for your purposes, use a binding-capable + * {@link Errors} implementation such as {@link BeanPropertyBindingResult}. + * + * @author Juergen Hoeller + * @since 6.1 + * @see Validator#validateObject(Object) + * @see BeanPropertyBindingResult + * @see DirectFieldBindingResult + */ +@SuppressWarnings("serial") +public class SimpleErrors implements Errors, Serializable { + + private final Object target; + + private final String objectName; + + private final List globalErrors = new ArrayList<>(); + + private final List fieldErrors = new ArrayList<>(); + + + /** + * Create a new {@link SimpleErrors} holder for the given target, + * using the simple name of the target class as the object name. + * @param target the target to wrap + */ + public SimpleErrors(Object target) { + Assert.notNull(target, "Target must not be null"); + this.target = target; + this.objectName = this.target.getClass().getSimpleName(); + } + + /** + * Create a new {@link SimpleErrors} holder for the given target. + * @param target the target to wrap + * @param objectName the name of the target object for error reporting + */ + public SimpleErrors(Object target, String objectName) { + Assert.notNull(target, "Target must not be null"); + this.target = target; + this.objectName = objectName; + } + + + @Override + public String getObjectName() { + return this.objectName; + } + + @Override + public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.globalErrors.add(new ObjectError(getObjectName(), new String[] {errorCode}, errorArgs, defaultMessage)); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + + if (!StringUtils.hasLength(field)) { + reject(errorCode, errorArgs, defaultMessage); + return; + } + + Object newVal = getFieldValue(field); + this.fieldErrors.add(new FieldError(getObjectName(), field, newVal, false, + new String[] {errorCode}, errorArgs, defaultMessage)); + } + + @Override + public void addAllErrors(Errors errors) { + this.globalErrors.addAll(errors.getGlobalErrors()); + this.fieldErrors.addAll(errors.getFieldErrors()); + } + + @Override + public List getGlobalErrors() { + return this.globalErrors; + } + + @Override + public List getFieldErrors() { + return this.fieldErrors; + } + + @Override + @Nullable + public Object getFieldValue(String field) { + FieldError fieldError = getFieldError(field); + if (fieldError != null) { + return fieldError.getRejectedValue(); + } + + PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(this.target.getClass(), field); + if (pd != null && pd.getReadMethod() != null) { + ReflectionUtils.makeAccessible(pd.getReadMethod()); + return ReflectionUtils.invokeMethod(pd.getReadMethod(), this.target); + } + + Field rawField = ReflectionUtils.findField(this.target.getClass(), field); + if (rawField != null) { + ReflectionUtils.makeAccessible(rawField); + return ReflectionUtils.getField(rawField, this.target); + } + + throw new IllegalArgumentException("Cannot retrieve value for field '" + field + + "' - neither a getter method nor a raw field found"); + } + + @Override + @Nullable + public Class getFieldType(String field) { + PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(this.target.getClass(), field); + if (pd != null) { + return pd.getPropertyType(); + } + Field rawField = ReflectionUtils.findField(this.target.getClass(), field); + if (rawField != null) { + return rawField.getType(); + } + return null; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof SimpleErrors that && + ObjectUtils.nullSafeEquals(this.target, that.target) && + this.globalErrors.equals(that.globalErrors) && + this.fieldErrors.equals(that.fieldErrors))); + } + + @Override + public int hashCode() { + return this.target.hashCode(); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (ObjectError error : this.globalErrors) { + sb.append('\n').append(error); + } + for (ObjectError error : this.fieldErrors) { + sb.append('\n').append(error); + } + return sb.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/SmartValidator.java b/spring-context/src/main/java/org/springframework/validation/SmartValidator.java index 36ad9f588d60..c033a9266dac 100644 --- a/spring-context/src/main/java/org/springframework/validation/SmartValidator.java +++ b/spring-context/src/main/java/org/springframework/validation/SmartValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,4 +64,19 @@ default void validateValue( throw new IllegalArgumentException("Cannot validate individual value for " + targetType); } + /** + * Return a contained validator instance of the specified type, unwrapping + * as far as necessary. + * @param type the class of the object to return + * @param the type of the object to return + * @return a validator instance of the specified type; {@code null} if there + * isn't a nested validator; an exception may be raised if the specified + * validator type does not match. + * @since 6.1 + */ + @Nullable + default T unwrap(@Nullable Class type) { + return null; + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/TypedValidator.java b/spring-context/src/main/java/org/springframework/validation/TypedValidator.java new file mode 100644 index 000000000000..000e9389b1eb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/TypedValidator.java @@ -0,0 +1,63 @@ +/* + * 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.validation; + +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +import org.springframework.util.Assert; + +/** + * Validator instance returned by {@link Validator#forInstanceOf(Class, BiConsumer)} + * and {@link Validator#forType(Class, BiConsumer)}. + * + * @author Toshiaki Maki + * @author Arjen Poutsma + * @since 6.1 + * @param the target object type + */ +final class TypedValidator implements Validator { + + private final Class targetClass; + + private final Predicate> supports; + + private final BiConsumer validate; + + + public TypedValidator(Class targetClass, Predicate> supports, BiConsumer validate) { + Assert.notNull(targetClass, "TargetClass must not be null"); + Assert.notNull(supports, "Supports function must not be null"); + Assert.notNull(validate, "Validate function must not be null"); + + this.targetClass = targetClass; + this.supports = supports; + this.validate = validate; + } + + + @Override + public boolean supports(Class clazz) { + return this.supports.test(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + this.validate.accept(this.targetClass.cast(target), errors); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index 2aa282396358..bea33261aeb7 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -16,6 +16,8 @@ package org.springframework.validation; +import java.util.function.BiConsumer; + /** * A validator for application-specific objects. * @@ -26,38 +28,33 @@ * of an application, and supports the encapsulation of validation * logic as a first-class citizen in its own right. * - *

Find below a simple but complete {@code Validator} - * implementation, which validates that the various {@link String} - * properties of a {@code UserLogin} instance are not empty - * (that is they are not {@code null} and do not consist + *

Implementations can be created via the static factory methods + * {@link #forInstanceOf(Class, BiConsumer)} or + * {@link #forType(Class, BiConsumer)}. + * Below is a simple but complete {@code Validator} that validates that the + * various {@link String} properties of a {@code UserLogin} instance are not + * empty (they are not {@code null} and do not consist * wholly of whitespace), and that any password that is present is * at least {@code 'MINIMUM_PASSWORD_LENGTH'} characters in length. * - *

public class UserLoginValidator implements Validator {
- *
- *    private static final int MINIMUM_PASSWORD_LENGTH = 6;
- *
- *    public boolean supports(Class clazz) {
- *       return UserLogin.class.isAssignableFrom(clazz);
- *    }
- *
- *    public void validate(Object target, Errors errors) {
- *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
- *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
- *       UserLogin login = (UserLogin) target;
- *       if (login.getPassword() != null
- *             && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
- *          errors.rejectValue("password", "field.min.length",
- *                new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
- *                "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
- *       }
- *    }
- * }
+ *
Validator userLoginValidator = Validator.forInstance(UserLogin.class, (login, errors) -> {
+ *   ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
+ *   ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
+ *   if (login.getPassword() != null
+ *         && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
+ *      errors.rejectValue("password", "field.min.length",
+ *            new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
+ *            "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
+ *   }
+ * });
* *

See also the Spring reference manual for a fuller discussion of the * {@code Validator} interface and its role in an enterprise application. * * @author Rod Johnson + * @author Juergen Hoeller + * @author Toshiaki Maki + * @author Arjen Poutsma * @see SmartValidator * @see Errors * @see ValidationUtils @@ -95,4 +92,75 @@ public interface Validator { */ void validate(Object target, Errors errors); + /** + * Validate the given {@code target} object individually. + *

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

Note: This validation call comes with limitations in the {@link Errors} + * implementation used, in particular no support for nested paths. + * If this is insufficient for your purposes, call the regular + * {@link #validate(Object, Errors)} method with a binding-capable + * {@link Errors} implementation such as {@link BeanPropertyBindingResult}. + * @param target the object that is to be validated + * @return resulting errors from the validation of the given object + * @since 6.1 + * @see SimpleErrors + */ + default Errors validateObject(Object target) { + Errors errors = new SimpleErrors(target); + validate(target, errors); + return errors; + } + + + /** + * Return a {@code Validator} that checks whether the target object + * {@linkplain Class#isAssignableFrom(Class) is an instance of} + * {@code targetClass}, applying the given {@code delegate} to populate + * {@link Errors} if it is. + *

For instance: + *

Validator passwordEqualsValidator = Validator.forInstanceOf(PasswordResetForm.class, (form, errors) -> {
+	 *   if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
+	 * 	   errors.rejectValue("confirmPassword",
+	 * 	         "PasswordEqualsValidator.passwordResetForm.password",
+	 * 	         "password and confirm password must be same.");
+	 * 	   }
+	 * 	 });
+ * @param targetClass the class supported by the returned validator + * @param delegate function invoked with the target object, if it is an + * instance of type T + * @param the target object type + * @return the created {@code Validator} + * @since 6.1 + */ + static Validator forInstanceOf(Class targetClass, BiConsumer delegate) { + return new TypedValidator<>(targetClass, targetClass::isAssignableFrom, delegate); + } + + /** + * Return a {@code Validator} that checks whether the target object's class + * is identical to {@code targetClass}, applying the given {@code delegate} + * to populate {@link Errors} if it is. + *

For instance: + *

Validator passwordEqualsValidator = Validator.forType(PasswordResetForm.class, (form, errors) -> {
+	 *   if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
+	 * 	   errors.rejectValue("confirmPassword",
+	 * 	         "PasswordEqualsValidator.passwordResetForm.password",
+	 * 	         "password and confirm password must be same.");
+	 * 	   }
+	 * 	 });
+ * @param targetClass the exact class supported by the returned validator (no subclasses) + * @param delegate function invoked with the target object, if it is an + * instance of type T + * @param the target object type + * @return the created {@code Validator} + * @since 6.1 + */ + static Validator forType(Class targetClass, BiConsumer delegate) { + return new TypedValidator<>(targetClass, targetClass::equals, delegate); + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java index 7604efd54c70..d13639a0c08c 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java @@ -131,18 +131,18 @@ else if (ex instanceof TypeNotPresentException) { constraintDescriptors.addAll(propertyDescriptor.getConstraintDescriptors()); } if (!constraintDescriptors.isEmpty()) { - return new BeanValidationBeanRegistrationAotContribution(constraintDescriptors); + return new AotContribution(constraintDescriptors); } return null; } } - private static class BeanValidationBeanRegistrationAotContribution implements BeanRegistrationAotContribution { + private static class AotContribution implements BeanRegistrationAotContribution { private final Collection> constraintDescriptors; - public BeanValidationBeanRegistrationAotContribution(Collection> constraintDescriptors) { + public AotContribution(Collection> constraintDescriptors) { this.constraintDescriptors = constraintDescriptors; } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java new file mode 100644 index 000000000000..b16ec4c2adda --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -0,0 +1,603 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.beanvalidation; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ElementKind; +import jakarta.validation.Path; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.executable.ExecutableValidator; +import jakarta.validation.metadata.ConstraintDescriptor; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.Conventions; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.DefaultMessageCodesResolver; +import org.springframework.validation.Errors; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.annotation.Validated; +import org.springframework.validation.method.MethodValidationResult; +import org.springframework.validation.method.MethodValidator; +import org.springframework.validation.method.ParameterErrors; +import org.springframework.validation.method.ParameterValidationResult; + +/** + * {@link MethodValidator} that uses a Bean Validation + * {@link jakarta.validation.Validator} for validation, and adapts + * {@link ConstraintViolation}s to {@link MethodValidationResult}. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +public class MethodValidationAdapter implements MethodValidator { + + private static final MethodValidationResult emptyValidationResult = MethodValidationResult.emptyResult(); + + private static final ObjectNameResolver defaultObjectNameResolver = new DefaultObjectNameResolver(); + + private static final Comparator resultComparator = new ResultComparator(); + + + private final Supplier validator; + + private final Supplier validatorAdapter; + + private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); + + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + private ObjectNameResolver objectNameResolver = defaultObjectNameResolver; + + + /** + * Create an instance using a default JSR-303 validator underneath. + */ + public MethodValidationAdapter() { + this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator()); + this.validatorAdapter = initValidatorAdapter(this.validator); + } + + /** + * Create an instance using the given JSR-303 ValidatorFactory. + * @param validatorFactory the JSR-303 ValidatorFactory to use + */ + @SuppressWarnings("DataFlowIssue") + public MethodValidationAdapter(ValidatorFactory validatorFactory) { + if (validatorFactory instanceof SpringValidatorAdapter adapter) { + this.validator = () -> adapter; + this.validatorAdapter = () -> adapter; + } + else { + this.validator = SingletonSupplier.of(validatorFactory::getValidator); + this.validatorAdapter = SingletonSupplier.of(() -> new SpringValidatorAdapter(this.validator.get())); + } + } + + /** + * Create an instance using the given JSR-303 Validator. + * @param validator the JSR-303 Validator to use + */ + public MethodValidationAdapter(Validator validator) { + this.validator = () -> validator; + this.validatorAdapter = initValidatorAdapter(this.validator); + } + + /** + * Create an instance for the supplied (potentially lazily initialized) Validator. + * @param validator a Supplier for the Validator to use + */ + public MethodValidationAdapter(Supplier validator) { + this.validator = validator; + this.validatorAdapter = initValidatorAdapter(validator); + } + + private static Supplier initValidatorAdapter(Supplier validatorSupplier) { + return SingletonSupplier.of(() -> { + Validator validator = validatorSupplier.get(); + return (validator instanceof SpringValidatorAdapter sva ? sva : new SpringValidatorAdapter(validator)); + }); + } + + + /** + * Return the {@link SpringValidatorAdapter} configured for use. + */ + public Supplier getSpringValidatorAdapter() { + return this.validatorAdapter; + } + + /** + * Set the strategy to use to determine message codes for violations. + *

Default is a DefaultMessageCodesResolver. + */ + public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + } + + /** + * Return the {@link #setMessageCodesResolver(MessageCodesResolver) configured} + * {@code MessageCodesResolver}. + */ + public MessageCodesResolver getMessageCodesResolver() { + return this.messageCodesResolver; + } + + /** + * Set the {@code ParameterNameDiscoverer} to discover method parameter names + * with to create error codes for {@link MessageSourceResolvable}. Used only + * when {@link MethodParameter}s are not passed into + * {@link #validateArguments} or {@link #validateReturnValue}. + *

Default is {@link org.springframework.core.DefaultParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * Return the {@link #setParameterNameDiscoverer configured} + * {@code ParameterNameDiscoverer}. + */ + public ParameterNameDiscoverer getParameterNameDiscoverer() { + return this.parameterNameDiscoverer; + } + + /** + * Configure a resolver to determine the name of an {@code @Valid} method + * parameter to use for its {@link BindingResult}. This allows aligning with + * a higher level programming model such as to resolve the name of an + * {@code @ModelAttribute} method parameter in Spring MVC. + *

By default, the object name is resolved through: + *

    + *
  • {@link MethodParameter#getParameterName()} for input parameters + *
  • {@link Conventions#getVariableNameForReturnType(Method, Class, Object)} + * for a return type + *
+ * If a name cannot be determined, e.g. a return value with insufficient + * type information, then it defaults to one of: + *
    + *
  • {@code "{methodName}.arg{index}"} for input parameters + *
  • {@code "{methodName}.returnValue"} for a return type + *
+ */ + public void setObjectNameResolver(ObjectNameResolver nameResolver) { + this.objectNameResolver = nameResolver; + } + + + /** + * {@inheritDoc}. + *

Default are the validation groups as specified in the {@link Validated} + * annotation on the method, or on the containing target class of the method, + * or for an AOP proxy without a target (with all behavior in advisors), also + * check on proxied interfaces. + */ + @Override + public Class[] determineValidationGroups(Object target, Method method) { + Validated validatedAnn = AnnotationUtils.findAnnotation(method, Validated.class); + if (validatedAnn == null) { + if (AopUtils.isAopProxy(target)) { + for (Class type : AopProxyUtils.proxiedUserInterfaces(target)) { + validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class); + if (validatedAnn != null) { + break; + } + } + } + else { + validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class); + } + } + return (validatedAnn != null ? validatedAnn.value() : new Class[0]); + } + + @Override + public final MethodValidationResult validateArguments( + Object target, Method method, @Nullable MethodParameter[] parameters, + Object[] arguments, Class[] groups) { + + Set> violations = + invokeValidatorForArguments(target, method, arguments, groups); + + if (violations.isEmpty()) { + return emptyValidationResult; + } + + return adaptViolations(target, method, violations, + i -> (parameters != null ? parameters[i] : initMethodParameter(method, i)), + i -> arguments[i]); + } + + /** + * Invoke the validator, and return the resulting violations. + */ + public final Set> invokeValidatorForArguments( + Object target, Method method, Object[] arguments, Class[] groups) { + + ExecutableValidator execVal = this.validator.get().forExecutables(); + try { + return execVal.validateParameters(target, method, arguments, groups); + } + catch (IllegalArgumentException ex) { + // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 + // Let's try to find the bridged method on the implementation class... + Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(method, target.getClass()); + return execVal.validateParameters(target, bridgedMethod, arguments, groups); + } + } + + @Override + public final MethodValidationResult validateReturnValue( + Object target, Method method, @Nullable MethodParameter returnType, + @Nullable Object returnValue, Class[] groups) { + + Set> violations = + invokeValidatorForReturnValue(target, method, returnValue, groups); + + if (violations.isEmpty()) { + return emptyValidationResult; + } + + return adaptViolations(target, method, violations, + i -> (returnType != null ? returnType : initMethodParameter(method, -1)), + i -> returnValue); + } + + /** + * Invoke the validator, and return the resulting violations. + */ + public final Set> invokeValidatorForReturnValue( + Object target, Method method, @Nullable Object returnValue, Class[] groups) { + + ExecutableValidator execVal = this.validator.get().forExecutables(); + return execVal.validateReturnValue(target, method, returnValue, groups); + } + + private MethodValidationResult adaptViolations( + Object target, Method method, Set> violations, + Function parameterFunction, + Function argumentFunction) { + + Map paramViolations = new LinkedHashMap<>(); + Map nestedViolations = new LinkedHashMap<>(); + + for (ConstraintViolation violation : violations) { + Iterator nodes = violation.getPropertyPath().iterator(); + while (nodes.hasNext()) { + Path.Node node = nodes.next(); + + MethodParameter parameter; + if (node.getKind().equals(ElementKind.PARAMETER)) { + int index = node.as(Path.ParameterNode.class).getParameterIndex(); + parameter = parameterFunction.apply(index); + } + else if (node.getKind().equals(ElementKind.RETURN_VALUE)) { + parameter = parameterFunction.apply(-1); + } + else { + continue; + } + + Object arg = argumentFunction.apply(parameter.getParameterIndex()); + + // If the arg is a container, we need the element, but the only way to extract it + // is to check for and use a container index or key on the next node: + // https://github.com/jakartaee/validation/issues/194 + + Path.Node parameterNode = node; + if (nodes.hasNext()) { + node = nodes.next(); + } + + Object value; + Object container; + Integer index = node.getIndex(); + Object key = node.getKey(); + if (index != null && arg instanceof List list) { + value = list.get(index); + container = list; + } + else if (index != null && arg instanceof Object[] array) { + value = array[index]; + container = array; + } + else if (key != null && arg instanceof Map map) { + value = map.get(key); + container = map; + } + else if (arg instanceof Iterable) { + // No index or key, cannot access the specific value + value = arg; + container = arg; + } + else if (arg instanceof Optional optional) { + value = optional.orElse(null); + container = optional; + } + else { + value = arg; + container = null; + } + + if (node.getKind().equals(ElementKind.PROPERTY)) { + nestedViolations + .computeIfAbsent(parameterNode, k -> + new ParamErrorsBuilder(parameter, value, container, index, key)) + .addViolation(violation); + } + else { + paramViolations + .computeIfAbsent(parameterNode, p -> + new ParamValidationResultBuilder(target, parameter, value, container, index, key)) + .addViolation(violation); + } + + break; + } + } + + List resultList = new ArrayList<>(); + paramViolations.forEach((param, builder) -> resultList.add(builder.build())); + nestedViolations.forEach((key, builder) -> resultList.add(builder.build())); + resultList.sort(resultComparator); + + return MethodValidationResult.create(target, method, resultList); + } + + private MethodParameter initMethodParameter(Method method, int index) { + MethodParameter parameter = new MethodParameter(method, index); + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + return parameter; + } + + private MessageSourceResolvable createMessageSourceResolvable( + Object target, MethodParameter parameter, ConstraintViolation violation) { + + String objectName = Conventions.getVariableName(target) + "#" + parameter.getExecutable().getName(); + String paramName = (parameter.getParameterName() != null ? parameter.getParameterName() : ""); + Class parameterType = parameter.getParameterType(); + + ConstraintDescriptor descriptor = violation.getConstraintDescriptor(); + String code = descriptor.getAnnotation().annotationType().getSimpleName(); + String[] codes = this.messageCodesResolver.resolveMessageCodes(code, objectName, paramName, parameterType); + Object[] arguments = this.validatorAdapter.get().getArgumentsForConstraint(objectName, paramName, descriptor); + + return new DefaultMessageSourceResolvable(codes, arguments, violation.getMessage()); + } + + private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) { + String objectName = this.objectNameResolver.resolveName(parameter, argument); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName); + result.setMessageCodesResolver(this.messageCodesResolver); + return result; + } + + + /** + * Strategy to resolve the name of an {@code @Valid} method parameter to + * use for its {@link BindingResult}. + */ + public interface ObjectNameResolver { + + /** + * Determine the name for the given method argument. + * @param parameter the method parameter + * @param value the argument value or return value + * @return the name to use + */ + String resolveName(MethodParameter parameter, @Nullable Object value); + } + + + /** + * Builds a validation result for a value method parameter with constraints + * declared directly on it. + */ + private final class ParamValidationResultBuilder { + + private final Object target; + + private final MethodParameter parameter; + + @Nullable + private final Object value; + + @Nullable + private final Object container; + + @Nullable + private final Integer containerIndex; + + @Nullable + private final Object containerKey; + + private final List resolvableErrors = new ArrayList<>(); + + public ParamValidationResultBuilder( + Object target, MethodParameter parameter, @Nullable Object value, @Nullable Object container, + @Nullable Integer containerIndex, @Nullable Object containerKey) { + + this.target = target; + this.parameter = parameter; + this.value = value; + this.container = container; + this.containerIndex = containerIndex; + this.containerKey = containerKey; + } + + public void addViolation(ConstraintViolation violation) { + this.resolvableErrors.add(createMessageSourceResolvable(this.target, this.parameter, violation)); + } + + public ParameterValidationResult build() { + return new ParameterValidationResult( + this.parameter, this.value, this.resolvableErrors, this.container, + this.containerIndex, this.containerKey); + } + } + + + /** + * Builds a validation result for an {@link jakarta.validation.Valid @Valid} + * annotated bean method parameter with cascaded constraints. + */ + private final class ParamErrorsBuilder { + + private final MethodParameter parameter; + + @Nullable + private final Object bean; + + @Nullable + private final Object container; + + @Nullable + private final Integer containerIndex; + + @Nullable + private final Object containerKey; + + private final Errors errors; + + private final Set> violations = new LinkedHashSet<>(); + + public ParamErrorsBuilder( + MethodParameter param, @Nullable Object bean, @Nullable Object container, + @Nullable Integer containerIndex, @Nullable Object containerKey) { + + this.parameter = param; + this.bean = bean; + this.container = container; + this.containerIndex = containerIndex; + this.containerKey = containerKey; + this.errors = createBindingResult(param, this.bean); + } + + public void addViolation(ConstraintViolation violation) { + this.violations.add(violation); + } + + public ParameterErrors build() { + validatorAdapter.get().processConstraintViolations(this.violations, this.errors); + return new ParameterErrors( + this.parameter, this.bean, this.errors, this.container, + this.containerIndex, this.containerKey); + } + } + + + /** + * Default algorithm to select an object name, as described in {@link #setObjectNameResolver}. + */ + private static class DefaultObjectNameResolver implements ObjectNameResolver { + + @Override + public String resolveName(MethodParameter parameter, @Nullable Object value) { + String objectName = null; + if (parameter.getParameterIndex() != -1) { + objectName = parameter.getParameterName(); + } + else { + try { + Method method = parameter.getMethod(); + if (method != null) { + Class containingClass = parameter.getContainingClass(); + Class resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); + objectName = Conventions.getVariableNameForReturnType(method, resolvedType, value); + } + } + catch (IllegalArgumentException ex) { + // insufficient type information + } + } + if (objectName == null) { + int index = parameter.getParameterIndex(); + objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ".returnValue")); + } + return objectName; + } + } + + + /** + * Comparator for validation results, sorted by method parameter index first, + * also falling back on container indexes if necessary for cascaded + * constraints on a List container. + */ + private static final class ResultComparator implements Comparator { + + @Override + public int compare(ParameterValidationResult result1, ParameterValidationResult result2) { + int index1 = result1.getMethodParameter().getParameterIndex(); + int index2 = result2.getMethodParameter().getParameterIndex(); + int i = Integer.compare(index1, index2); + if (i != 0) { + return i; + } + if (result1 instanceof ParameterErrors errors1 && result2 instanceof ParameterErrors errors2) { + Integer containerIndex1 = errors1.getContainerIndex(); + Integer containerIndex2 = errors2.getContainerIndex(); + if (containerIndex1 != null && containerIndex2 != null) { + i = Integer.compare(containerIndex1, containerIndex2); + if (i != 0) { + return i; + } + } + i = compareKeys(errors1, errors2); + return i; + } + return 0; + } + + @SuppressWarnings("unchecked") + private int compareKeys(ParameterErrors errors1, ParameterErrors errors2) { + Object key1 = errors1.getContainerKey(); + Object key2 = errors2.getContainerKey(); + if (key1 instanceof Comparable && key2 instanceof Comparable) { + return ((Comparable) key1).compareTo((E) key2); + } + return 0; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java index 059abb5d0d3b..50236152705b 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,41 +17,54 @@ package org.springframework.validation.beanvalidation; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Supplier; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validation; +import jakarta.validation.Valid; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; -import jakarta.validation.executable.ExecutableValidator; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.aop.ProxyMethodInvocation; -import org.springframework.aop.framework.AopProxyUtils; -import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.SmartFactoryBean; -import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.function.SingletonSupplier; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; import org.springframework.validation.annotation.Validated; +import org.springframework.validation.method.MethodValidationException; +import org.springframework.validation.method.MethodValidationResult; +import org.springframework.validation.method.ParameterErrors; +import org.springframework.validation.method.ParameterValidationResult; /** * An AOP Alliance {@link MethodInterceptor} implementation that delegates to a * JSR-303 provider for performing method-level validation on annotated methods. * - *

Applicable methods have JSR-303 constraint annotations on their parameters - * and/or on their return value (in the latter case specified at the method level, - * typically as inline annotation). + *

Applicable methods have {@link jakarta.validation.Constraint} annotations on + * their parameters and/or on their return value (in the latter case specified at + * the method level, typically as inline annotation). * *

E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)} * + *

In case of validation errors, the interceptor can raise + * {@link ConstraintViolationException}, or adapt the violations to + * {@link MethodValidationResult} and raise {@link MethodValidationException}. + * *

Validation groups can be specified through Spring's {@link Validated} annotation * at the type level of the containing target class, applying to all public service methods * of that class. By default, JSR-303 will validate against its default group only. @@ -59,20 +72,27 @@ *

As of Spring 5.0, this functionality requires a Bean Validation 1.1+ provider. * * @author Juergen Hoeller + * @author Rossen Stoyanchev * @since 3.1 * @see MethodValidationPostProcessor * @see jakarta.validation.executable.ExecutableValidator */ public class MethodValidationInterceptor implements MethodInterceptor { - private final Supplier validator; + private static final boolean reactorPresent = ClassUtils.isPresent( + "reactor.core.publisher.Mono", MethodValidationInterceptor.class.getClassLoader()); + + + private final MethodValidationAdapter validationAdapter; + + private final boolean adaptViolations; /** * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath. */ public MethodValidationInterceptor() { - this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator()); + this(new MethodValidationAdapter(), false); } /** @@ -80,7 +100,7 @@ public MethodValidationInterceptor() { * @param validatorFactory the JSR-303 ValidatorFactory to use */ public MethodValidationInterceptor(ValidatorFactory validatorFactory) { - this.validator = SingletonSupplier.of(validatorFactory::getValidator); + this(new MethodValidationAdapter(validatorFactory), false); } /** @@ -88,7 +108,7 @@ public MethodValidationInterceptor(ValidatorFactory validatorFactory) { * @param validator the JSR-303 Validator to use */ public MethodValidationInterceptor(Validator validator) { - this.validator = () -> validator; + this(new MethodValidationAdapter(validator), false); } /** @@ -98,7 +118,25 @@ public MethodValidationInterceptor(Validator validator) { * @since 6.0 */ public MethodValidationInterceptor(Supplier validator) { - this.validator = validator; + this(validator, false); + } + + /** + * Create a new MethodValidationInterceptor for the supplied + * (potentially lazily initialized) Validator. + * @param validator a Supplier for the Validator to use + * @param adaptViolations whether to adapt {@link ConstraintViolation}s, and + * if {@code true}, raise {@link MethodValidationException}, of if + * {@code false} raise {@link ConstraintViolationException} instead + * @since 6.1 + */ + public MethodValidationInterceptor(Supplier validator, boolean adaptViolations) { + this(new MethodValidationAdapter(validator), adaptViolations); + } + + private MethodValidationInterceptor(MethodValidationAdapter validationAdapter, boolean adaptViolations) { + this.validationAdapter = validationAdapter; + this.adaptViolations = adaptViolations; } @@ -110,44 +148,54 @@ public Object invoke(MethodInvocation invocation) throws Throwable { return invocation.proceed(); } + Object target = getTarget(invocation); + Method method = invocation.getMethod(); + Object[] arguments = invocation.getArguments(); Class[] groups = determineValidationGroups(invocation); - // Standard Bean Validation 1.1 API - ExecutableValidator execVal = this.validator.get().forExecutables(); - Method methodToValidate = invocation.getMethod(); - Set> result; - - Object target = invocation.getThis(); - if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) { - // Allow validation for AOP proxy without a target - target = methodInvocation.getProxy(); + if (reactorPresent) { + arguments = ReactorValidationHelper.insertAsyncValidation( + this.validationAdapter.getSpringValidatorAdapter(), this.adaptViolations, + target, method, arguments); } - Assert.state(target != null, "Target must not be null"); - try { - result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); - } - catch (IllegalArgumentException ex) { - // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 - // Let's try to find the bridged method on the implementation class... - methodToValidate = BridgeMethodResolver.findBridgedMethod( - ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass())); - result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); + Set> violations; + + if (this.adaptViolations) { + this.validationAdapter.applyArgumentValidation(target, method, null, arguments, groups); } - if (!result.isEmpty()) { - throw new ConstraintViolationException(result); + else { + violations = this.validationAdapter.invokeValidatorForArguments(target, method, arguments, groups); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } } Object returnValue = invocation.proceed(); - result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups); - if (!result.isEmpty()) { - throw new ConstraintViolationException(result); + if (this.adaptViolations) { + this.validationAdapter.applyReturnValueValidation(target, method, null, returnValue, groups); + } + else { + violations = this.validationAdapter.invokeValidatorForReturnValue(target, method, returnValue, groups); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } } return returnValue; } + private static Object getTarget(MethodInvocation invocation) { + Object target = invocation.getThis(); + if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) { + // Allow validation for AOP proxy without a target + target = methodInvocation.getProxy(); + } + Assert.state(target != null, "Target must not be null"); + return target; + } + private boolean isFactoryBeanMetadataMethod(Method method) { Class clazz = method.getDeclaringClass(); @@ -178,25 +226,82 @@ else if (FactoryBean.class.isAssignableFrom(clazz)) { * @return the applicable validation groups as a Class array */ protected Class[] determineValidationGroups(MethodInvocation invocation) { - Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class); - if (validatedAnn == null) { - Object target = invocation.getThis(); - if (target != null) { - validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class); + Object target = getTarget(invocation); + return this.validationAdapter.determineValidationGroups(target, invocation.getMethod()); + } + + + /** + * Helper class to decorate reactive arguments with async validation. + */ + private static final class ReactorValidationHelper { + + private static final ReactiveAdapterRegistry reactiveAdapterRegistry = + ReactiveAdapterRegistry.getSharedInstance(); + + + static Object[] insertAsyncValidation( + Supplier validatorAdapterSupplier, boolean adaptViolations, + Object target, Method method, Object[] arguments) { + + for (int i = 0; i < method.getParameterCount(); i++) { + if (arguments[i] == null) { + continue; + } + Class parameterType = method.getParameterTypes()[i]; + ReactiveAdapter reactiveAdapter = reactiveAdapterRegistry.getAdapter(parameterType); + if (reactiveAdapter == null || reactiveAdapter.isNoValue()) { + continue; + } + Class[] groups = determineValidationGroups(method.getParameters()[i]); + if (groups == null) { + continue; + } + SpringValidatorAdapter validatorAdapter = validatorAdapterSupplier.get(); + MethodParameter param = new MethodParameter(method, i); + arguments[i] = (reactiveAdapter.isMultiValue() ? + Flux.from(reactiveAdapter.toPublisher(arguments[i])).doOnNext(value -> + validate(validatorAdapter, adaptViolations, target, method, param, value, groups)) : + Mono.from(reactiveAdapter.toPublisher(arguments[i])).doOnNext(value -> + validate(validatorAdapter, adaptViolations, target, method, param, value, groups))); + } + return arguments; + } + + @Nullable + private static Class[] determineValidationGroups(Parameter parameter) { + Validated validated = AnnotationUtils.findAnnotation(parameter, Validated.class); + if (validated != null) { + return validated.value(); + } + Valid valid = AnnotationUtils.findAnnotation(parameter, Valid.class); + if (valid != null) { + return new Class[0]; + } + return null; + } + + @SuppressWarnings("unchecked") + private static void validate( + SpringValidatorAdapter validatorAdapter, boolean adaptViolations, + Object target, Method method, MethodParameter parameter, Object argument, Class[] groups) { + + if (adaptViolations) { + Errors errors = new BeanPropertyBindingResult(argument, argument.getClass().getSimpleName()); + validatorAdapter.validate(argument, errors); + if (errors.hasErrors()) { + ParameterErrors paramErrors = new ParameterErrors(parameter, argument, errors, null, null, null); + List results = Collections.singletonList(paramErrors); + throw new MethodValidationException(MethodValidationResult.create(target, method, results)); + } } - else if (invocation instanceof ProxyMethodInvocation methodInvocation) { - Object proxy = methodInvocation.getProxy(); - if (AopUtils.isAopProxy(proxy)) { - for (Class type : AopProxyUtils.proxiedUserInterfaces(proxy)) { - validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class); - if (validatedAnn != null) { - break; - } - } + else { + Set> violations = validatorAdapter.validate((T) argument, groups); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); } } } - return (validatedAnn != null ? validatedAnn.value() : new Class[0]); } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java index e0b14b865a44..e7add2e73e87 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.lang.annotation.Annotation; import java.util.function.Supplier; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; @@ -34,6 +36,8 @@ import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; import org.springframework.validation.annotation.Validated; +import org.springframework.validation.method.MethodValidationException; +import org.springframework.validation.method.MethodValidationResult; /** * A convenient {@link BeanPostProcessor} implementation that delegates to a @@ -47,6 +51,10 @@ * public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2) * * + *

In case of validation errors, the interceptor can raise + * {@link ConstraintViolationException}, or adapt the violations to + * {@link MethodValidationResult} and raise {@link MethodValidationException}. + * *

Target classes with such annotated methods need to be annotated with Spring's * {@link Validated} annotation at the type level, for their methods to be searched for * inline constraint annotations. Validation groups can be specified through {@code @Validated} @@ -68,6 +76,8 @@ public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvis private Supplier validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator()); + private boolean adaptConstraintViolations; + /** * Set the 'validated' annotation type. @@ -109,6 +119,18 @@ public void setValidatorProvider(ObjectProvider validatorProvider) { this.validator = validatorProvider::getObject; } + /** + * Whether to adapt {@link ConstraintViolation}s to {@link MethodValidationResult}. + *

By default {@code false} in which case + * {@link jakarta.validation.ConstraintViolationException} is raised in case of + * violations. When set to {@code true}, {@link MethodValidationException} + * is raised instead with the method validation results. + * @since 6.1 + */ + public void setAdaptConstraintViolations(boolean adaptViolations) { + this.adaptConstraintViolations = adaptViolations; + } + @Override public void afterPropertiesSet() { @@ -125,7 +147,7 @@ public void afterPropertiesSet() { * @since 6.0 */ protected Advice createMethodValidationAdvice(Supplier validator) { - return new MethodValidationInterceptor(validator); + return new MethodValidationInterceptor(validator, this.adaptConstraintViolations); } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index 44a19adf41d6..336fb6e81fbc 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,12 +32,14 @@ import jakarta.validation.metadata.BeanDescriptor; import jakarta.validation.metadata.ConstraintDescriptor; +import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.NotReadablePropertyException; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; @@ -201,7 +203,7 @@ protected String determineField(ConstraintViolation violation) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Path.Node node : path) { - if (node.isInIterable()) { + if (node.isInIterable() && !first) { sb.append('['); Object index = node.getIndex(); if (index == null) { @@ -286,7 +288,9 @@ protected Object[] getArgumentsForConstraint(String objectName, String field, Co * @see #getArgumentsForConstraint */ protected MessageSourceResolvable getResolvableField(String objectName, String field) { - String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; + String[] codes = (StringUtils.hasText(field) ? + new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field} : + new String[] {objectName}); return new DefaultMessageSourceResolvable(codes, field); } @@ -309,7 +313,13 @@ protected Object getRejectedValue(String field, ConstraintViolation viol (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) { // Possibly a bean constraint with property path: retrieve the actual property value. // However, explicitly avoid this for "address[]" style paths that we can't handle. - invalidValue = bindingResult.getRawFieldValue(field); + try { + invalidValue = bindingResult.getRawFieldValue(field); + } + catch (InvalidPropertyException ex) { + // Bean validation uses ValueExtractor's to unwrap container values + // in which cases we can't access the raw value. + } } return invalidValue; } diff --git a/spring-context/src/main/java/org/springframework/validation/method/DefaultMethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/DefaultMethodValidationResult.java new file mode 100644 index 000000000000..edb3caf393bc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/DefaultMethodValidationResult.java @@ -0,0 +1,80 @@ +/* + * 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.validation.method; + +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Default {@link MethodValidationResult} implementation as a simple container. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +final class DefaultMethodValidationResult implements MethodValidationResult { + + private final Object target; + + private final Method method; + + private final List allValidationResults; + + private final boolean forReturnValue; + + + DefaultMethodValidationResult(Object target, Method method, List results) { + Assert.notEmpty(results, "'results' is required and must not be empty"); + Assert.notNull(target, "'target' is required"); + Assert.notNull(method, "Method is required"); + this.target = target; + this.method = method; + this.allValidationResults = results; + this.forReturnValue = (results.get(0).getMethodParameter().getParameterIndex() == -1); + } + + + @Override + public Object getTarget() { + return this.target; + } + + @Override + public Method getMethod() { + return this.method; + } + + @Override + public boolean isForReturnValue() { + return this.forReturnValue; + } + + @Override + public List getAllValidationResults() { + return this.allValidationResults; + } + + + @Override + public String toString() { + return getAllErrors().size() + " validation errors " + + "for " + (isForReturnValue() ? "return value" : "arguments") + " of " + + this.method.toGenericString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/method/EmptyMethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/EmptyMethodValidationResult.java new file mode 100644 index 000000000000..484f683f106e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/EmptyMethodValidationResult.java @@ -0,0 +1,56 @@ +/* + * 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.validation.method; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +/** + * {@link MethodValidationResult} with an empty list of results. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +final class EmptyMethodValidationResult implements MethodValidationResult { + + @Override + public Object getTarget() { + throw new UnsupportedOperationException(); + } + + @Override + public Method getMethod() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isForReturnValue() { + throw new UnsupportedOperationException(); + } + + @Override + public List getAllValidationResults() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return "0 validation errors"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationException.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationException.java new file mode 100644 index 000000000000..5eabc69988e6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationException.java @@ -0,0 +1,64 @@ +/* + * 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.validation.method; + +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Exception that is a {@link MethodValidationResult}. + * + * @author Rossen Stoyanchev + * @since 6.1 + * @see MethodValidator + */ +@SuppressWarnings("serial") +public class MethodValidationException extends RuntimeException implements MethodValidationResult { + + private final MethodValidationResult validationResult; + + + public MethodValidationException(MethodValidationResult validationResult) { + super(validationResult.toString()); + Assert.notNull(validationResult, "MethodValidationResult is required"); + this.validationResult = validationResult; + } + + + @Override + public Object getTarget() { + return this.validationResult.getTarget(); + } + + @Override + public Method getMethod() { + return this.validationResult.getMethod(); + } + + @Override + public boolean isForReturnValue() { + return this.validationResult.isForReturnValue(); + } + + @Override + public List getAllValidationResults() { + return this.validationResult.getAllValidationResults(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java new file mode 100644 index 000000000000..015ecd14ba58 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java @@ -0,0 +1,127 @@ +/* + * 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.validation.method; + +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.context.MessageSourceResolvable; +import org.springframework.validation.Errors; + +/** + * Container for method validation results with validation errors from the + * underlying library adapted to {@link MessageSourceResolvable}s and grouped + * by method parameter as {@link ParameterValidationResult}. For method parameters + * with nested validation errors, the validation result is of type + * {@link ParameterErrors} and implements {@link Errors}. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +public interface MethodValidationResult { + + /** + * Return the target of the method invocation to which validation was applied. + */ + Object getTarget(); + + /** + * Return the method to which validation was applied. + */ + Method getMethod(); + + /** + * Whether the violations are for a return value. + * If true the violations are from validating a return value. + * If false the violations are from validating method arguments. + */ + boolean isForReturnValue(); + + /** + * Whether the result contains any validation errors. + */ + default boolean hasErrors() { + return !getAllValidationResults().isEmpty(); + } + + /** + * Return a single list with all errors from all validation results. + * @see #getAllValidationResults() + * @see ParameterValidationResult#getResolvableErrors() + */ + default List getAllErrors() { + return getAllValidationResults().stream() + .flatMap(result -> result.getResolvableErrors().stream()) + .toList(); + } + + /** + * Return all validation results. This includes both method parameters with + * errors directly on them, and Object method parameters with nested errors + * on their fields and properties. + * @see #getValueResults() + * @see #getBeanResults() + */ + List getAllValidationResults(); + + /** + * Return the subset of {@link #getAllValidationResults() allValidationResults} + * that includes method parameters with validation errors directly on method + * argument values. This excludes {@link #getBeanResults() beanResults} with + * nested errors on their fields and properties. + */ + default List getValueResults() { + return getAllValidationResults().stream() + .filter(result -> !(result instanceof ParameterErrors)) + .toList(); + } + + /** + * Return the subset of {@link #getAllValidationResults() allValidationResults} + * that includes Object method parameters with nested errors on their fields + * and properties. This excludes {@link #getValueResults() valueResults} with + * validation errors directly on method arguments. + */ + default List getBeanResults() { + return getAllValidationResults().stream() + .filter(result -> result instanceof ParameterErrors) + .map(result -> (ParameterErrors) result) + .toList(); + } + + + /** + * Factory method to create a {@link MethodValidationResult} instance. + * @param target the target Object + * @param method the target method + * @param results method validation results, expected to be non-empty + * @return the created instance + */ + static MethodValidationResult create(Object target, Method method, List results) { + return new DefaultMethodValidationResult(target, method, results); + } + + /** + * Factory method to create a {@link MethodValidationResult} instance with + * 0 errors, suitable to use as a constant. Getters for a target object or + * method are not supported. + */ + static MethodValidationResult emptyResult() { + return new EmptyMethodValidationResult(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidator.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidator.java new file mode 100644 index 000000000000..3c9e1fbf99ec --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidator.java @@ -0,0 +1,103 @@ +/* + * 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.validation.method; + +import java.lang.reflect.Method; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; + +/** + * Contract to apply method validation and handle the results. + * Exposes methods that return {@link MethodValidationResult}, and methods that + * handle the results, by default raising {@link MethodValidationException}. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +public interface MethodValidator { + + /** + * Determine the applicable validation groups. By default, obtained from an + * {@link org.springframework.validation.annotation.Validated @Validated} + * annotation on the method, or on the class level. + * @param target the target Object + * @param method the target method + * @return the applicable validation groups as a {@code Class} array + */ + Class[] determineValidationGroups(Object target, Method method); + + /** + * Validate the given method arguments and return validation results. + * @param target the target Object + * @param method the target method + * @param parameters the parameters, if already created and available + * @param arguments the candidate argument values to validate + * @param groups validation groups from {@link #determineValidationGroups} + * @return the result of validation + */ + MethodValidationResult validateArguments( + Object target, Method method, @Nullable MethodParameter[] parameters, + Object[] arguments, Class[] groups); + + /** + * Delegate to {@link #validateArguments} and handle the validation result, + * by default raising {@link MethodValidationException} in case of errors. + * Implementations may provide alternative handling, e.g. injecting + * {@link org.springframework.validation.Errors} into the method. + * @throws MethodValidationException in case of unhandled errors. + */ + default void applyArgumentValidation( + Object target, Method method, @Nullable MethodParameter[] parameters, + Object[] arguments, Class[] groups) { + + MethodValidationResult result = validateArguments(target, method, parameters, arguments, groups); + if (result.hasErrors()) { + throw new MethodValidationException(result); + } + } + + /** + * Validate the given return value and return validation results. + * @param target the target Object + * @param method the target method + * @param returnType the return parameter, if already created and available + * @param returnValue the return value to validate + * @param groups validation groups from {@link #determineValidationGroups} + * @return the result of validation + */ + MethodValidationResult validateReturnValue( + Object target, Method method, @Nullable MethodParameter returnType, + @Nullable Object returnValue, Class[] groups); + + /** + * Delegate to {@link #validateReturnValue} and handle the validation result, + * by default raising {@link MethodValidationException} in case of errors. + * Implementations may provide alternative handling. + * @throws MethodValidationException in case of unhandled errors. + */ + default void applyReturnValueValidation( + Object target, Method method, @Nullable MethodParameter returnType, + @Nullable Object returnValue, Class[] groups) { + + MethodValidationResult result = validateReturnValue(target, method, returnType, returnValue, groups); + if (result.hasErrors()) { + throw new MethodValidationException(result); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java b/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java new file mode 100644 index 000000000000..aa76afd0c2b1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.method; + +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +/** + * Extension of {@link ParameterValidationResult} created for Object method + * parameters or return values with nested errors on their properties. + * + *

The base class method {@link #getResolvableErrors()} returns + * {@link Errors#getAllErrors()}, but this subclass provides access to the same + * as {@link FieldError}s. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +public class ParameterErrors extends ParameterValidationResult implements Errors { + + private final Errors errors; + + + /** + * Create a {@code ParameterErrors}. + */ + public ParameterErrors( + MethodParameter parameter, @Nullable Object argument, Errors errors, + @Nullable Object container, @Nullable Integer index, @Nullable Object key) { + + super(parameter, argument, errors.getAllErrors(), container, index, key); + this.errors = errors; + } + + + // Errors implementation + + @Override + public String getObjectName() { + return this.errors.getObjectName(); + } + + @Override + public void setNestedPath(String nestedPath) { + this.errors.setNestedPath(nestedPath); + } + + @Override + public String getNestedPath() { + return this.errors.getNestedPath(); + } + + @Override + public void pushNestedPath(String subPath) { + this.errors.pushNestedPath(subPath); + } + + @Override + public void popNestedPath() throws IllegalStateException { + this.errors.popNestedPath(); + } + + @Override + public void reject(String errorCode) { + this.errors.reject(errorCode); + } + + @Override + public void reject(String errorCode, String defaultMessage) { + this.errors.reject(errorCode, defaultMessage); + } + + @Override + public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.errors.reject(errorCode, errorArgs, defaultMessage); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode) { + this.errors.rejectValue(field, errorCode); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { + this.errors.rejectValue(field, errorCode, defaultMessage); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + + this.errors.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + + @Override + public void addAllErrors(Errors errors) { + this.errors.addAllErrors(errors); + } + + @Override + public boolean hasErrors() { + return this.errors.hasErrors(); + } + + @Override + public int getErrorCount() { + return this.errors.getErrorCount(); + } + + @Override + public List getAllErrors() { + return this.errors.getAllErrors(); + } + + @Override + public boolean hasGlobalErrors() { + return this.errors.hasGlobalErrors(); + } + + @Override + public int getGlobalErrorCount() { + return this.errors.getGlobalErrorCount(); + } + + @Override + public List getGlobalErrors() { + return this.errors.getGlobalErrors(); + } + + @Override + @Nullable + public ObjectError getGlobalError() { + return this.errors.getGlobalError(); + } + + @Override + public boolean hasFieldErrors() { + return this.errors.hasFieldErrors(); + } + + @Override + public int getFieldErrorCount() { + return this.errors.getFieldErrorCount(); + } + + @Override + public List getFieldErrors() { + return this.errors.getFieldErrors(); + } + + @Override + @Nullable + public FieldError getFieldError() { + return this.errors.getFieldError(); + } + + @Override + public boolean hasFieldErrors(String field) { + return this.errors.hasFieldErrors(field); + } + + @Override + public int getFieldErrorCount(String field) { + return this.errors.getFieldErrorCount(field); + } + + @Override + public List getFieldErrors(String field) { + return this.errors.getFieldErrors(field); + } + + @Override + @Nullable + public FieldError getFieldError(String field) { + return this.errors.getFieldError(field); + } + + @Override + @Nullable + public Object getFieldValue(String field) { + return this.errors.getFieldError(field); + } + + @Override + @Nullable + public Class getFieldType(String field) { + return this.errors.getFieldType(field); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java new file mode 100644 index 000000000000..aeb0f3493bc5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.method; + +import java.util.Collection; +import java.util.List; + +import org.springframework.context.MessageSourceResolvable; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Store and expose the results of method validation for a method parameter. + *

    + *
  • Validation errors directly on method parameter values are exposed as a + * list of {@link MessageSourceResolvable}s. + *
  • Nested validation errors on an Object method parameter are exposed as + * {@link org.springframework.validation.Errors} by the subclass + * {@link ParameterErrors}. + *
+ * + *

When the method parameter is a container such as a {@link List}, array, + * or {@link java.util.Map}, then a separate {@link ParameterValidationResult} + * is created for each element with errors. In that case, the properties + * {@link #getContainer() container}, {@link #getContainerIndex() containerIndex}, + * and {@link #getContainerKey() containerKey} provide additional context. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +public class ParameterValidationResult { + + private final MethodParameter methodParameter; + + @Nullable + private final Object argument; + + private final List resolvableErrors; + + @Nullable + private final Object container; + + @Nullable + private final Integer containerIndex; + + @Nullable + private final Object containerKey; + + + /** + * Create a {@code ParameterValidationResult}. + */ + public ParameterValidationResult( + MethodParameter param, @Nullable Object arg, Collection errors, + @Nullable Object container, @Nullable Integer index, @Nullable Object key) { + + Assert.notNull(param, "MethodParameter is required"); + Assert.notEmpty(errors, "`resolvableErrors` must not be empty"); + this.methodParameter = param; + this.argument = arg; + this.resolvableErrors = List.copyOf(errors); + this.container = container; + this.containerIndex = index; + this.containerKey = key; + } + + /** + * Create a {@code ParameterValidationResult}. + * @deprecated in favor of + * {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object)} + */ + @Deprecated(since = "6.1.3", forRemoval = true) + public ParameterValidationResult( + MethodParameter param, @Nullable Object arg, Collection errors) { + + this(param, arg, errors, null, null, null); + } + + + /** + * The method parameter the validation results are for. + */ + public MethodParameter getMethodParameter() { + return this.methodParameter; + } + + /** + * The method argument value that was validated. + */ + @Nullable + public Object getArgument() { + return this.argument; + } + + /** + * List of {@link MessageSourceResolvable} representations adapted from the + * validation errors of the validation library. + *

    + *
  • For a constraints directly on a method parameter, error codes are + * based on the names of the constraint annotation, the object, the method, + * the parameter, and parameter type, e.g. + * {@code ["Max.myObject#myMethod.myParameter", "Max.myParameter", "Max.int", "Max"]}. + * Arguments include the parameter itself as a {@link MessageSourceResolvable}, e.g. + * {@code ["myObject#myMethod.myParameter", "myParameter"]}, followed by actual + * constraint annotation attributes (i.e. excluding "message", "groups" and + * "payload") in alphabetical order of attribute names. + *
  • For cascaded constraints via {@link jakarta.validation.Validator @Valid} + * on a bean method parameter, this method returns + * {@link org.springframework.validation.FieldError field errors} that you + * can also access more conveniently through methods of the + * {@link ParameterErrors} sub-class. + *
+ */ + public List getResolvableErrors() { + return this.resolvableErrors; + } + + /** + * When {@code @Valid} is declared on a container of elements such as + * {@link java.util.Collection}, {@link java.util.Map}, + * {@link java.util.Optional}, and others, this method returns the container + * of the validated {@link #getArgument() argument}, while + * {@link #getContainerIndex()} and {@link #getContainerKey()} provide + * information about the index or key if applicable. + */ + @Nullable + public Object getContainer() { + return this.container; + } + + /** + * When {@code @Valid} is declared on an indexed container of elements such as + * {@link List} or array, this method returns the index of the validated + * {@link #getArgument() argument}. + */ + @Nullable + public Integer getContainerIndex() { + return this.containerIndex; + } + + /** + * When {@code @Valid} is declared on a container of elements referenced by + * key such as {@link java.util.Map}, this method returns the key of the + * validated {@link #getArgument() argument}. + */ + @Nullable + public Object getContainerKey() { + return this.containerKey; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!super.equals(other)) { + return false; + } + ParameterValidationResult otherResult = (ParameterValidationResult) other; + return (getMethodParameter().equals(otherResult.getMethodParameter()) && + ObjectUtils.nullSafeEquals(getArgument(), otherResult.getArgument()) && + ObjectUtils.nullSafeEquals(getContainerIndex(), otherResult.getContainerIndex()) && + ObjectUtils.nullSafeEquals(getContainerKey(), otherResult.getContainerKey())); + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + hashCode = 29 * hashCode + getMethodParameter().hashCode(); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArgument()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getContainerIndex()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getContainerKey()); + return hashCode; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " for " + this.methodParameter + + ", argument value '" + ObjectUtils.nullSafeConciseToString(this.argument) + "'," + + (this.containerIndex != null ? "containerIndex[" + this.containerIndex + "]," : "") + + (this.containerKey != null ? "containerKey['" + this.containerKey + "']," : "") + + " errors: " + getResolvableErrors(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/method/package-info.java b/spring-context/src/main/java/org/springframework/validation/method/package-info.java new file mode 100644 index 000000000000..001ea1b69c45 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/method/package-info.java @@ -0,0 +1,21 @@ +/** + * Abstractions and support classes for method validation, independent of the + * underlying validation library. + * + *

The main abstractions: + *

    + *
  • {@link org.springframework.validation.method.MethodValidator} to apply + * method validation, and return or handle the results. + *
  • {@link org.springframework.validation.method.MethodValidationResult} and + * related types to represent the results. + *
  • {@link org.springframework.validation.method.MethodValidationException} + * to expose method validation results. + *
+ */ + +@NonNullApi +@NonNullFields +package org.springframework.validation.method; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt index 262377181281..c0cc8b573ace 100644 --- a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt +++ b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt @@ -21,6 +21,7 @@ import org.springframework.beans.factory.ObjectProvider import org.springframework.beans.factory.config.BeanDefinition import org.springframework.beans.factory.config.BeanDefinitionCustomizer import org.springframework.beans.factory.getBeanProvider +import org.springframework.beans.factory.support.AbstractBeanDefinition import org.springframework.beans.factory.support.BeanDefinitionReaderUtils import org.springframework.context.ApplicationContextInitializer import org.springframework.core.env.ConfigurableEnvironment @@ -165,6 +166,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition */ @@ -176,7 +178,8 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: Role? = null) { + role: Role? = null, + order: Int? = null) { val customizer = BeanDefinitionCustomizer { bd -> scope?.let { bd.scope = scope.name.lowercase() } @@ -186,7 +189,8 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit initMethodName?.let { bd.initMethodName = initMethodName } destroyMethodName?.let { bd.destroyMethodName = destroyMethodName } description?.let { bd.description = description } - role?. let { bd.role = role.ordinal } + role?.let { bd.role = role.ordinal } + order?.let { bd.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, order) } } val beanName = name ?: BeanDefinitionReaderUtils.uniqueBeanName(T::class.java.name, context); @@ -207,6 +211,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition * @param function the bean supplier function + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition */ @@ -219,6 +224,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit destroyMethodName: String? = null, description: String? = null, role: Role? = null, + order: Int? = null, crossinline function: BeanSupplierContext.() -> T) { val customizer = BeanDefinitionCustomizer { bd -> @@ -229,7 +235,8 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit initMethodName?.let { bd.initMethodName = initMethodName } destroyMethodName?.let { bd.destroyMethodName = destroyMethodName } description?.let { bd.description = description } - role?. let { bd.role = role.ordinal } + role?.let { bd.role = role.ordinal } + order?.let { bd.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, order) } } @@ -252,6 +259,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2.3 @@ -259,16 +267,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit inline fun bean(crossinline f: () -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke() } } @@ -288,6 +297,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -295,16 +305,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit inline fun bean(crossinline f: (A) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref()) } } @@ -324,6 +335,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -331,16 +343,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit inline fun bean(crossinline f: (A, B) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref()) } } @@ -360,6 +373,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -367,16 +381,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit inline fun bean(crossinline f: (A, B, C) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref()) } } @@ -396,6 +411,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -403,16 +419,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit inline fun bean(crossinline f: (A, B, C, D) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref()) } } @@ -432,6 +449,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -439,16 +457,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit inline fun bean(crossinline f: (A, B, C, D, E) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, - isLazyInit: Boolean? = null, - isPrimary: Boolean? = null, - isAutowireCandidate: Boolean? = null, - initMethodName: String? = null, - destroyMethodName: String? = null, - description: String? = null, - role: BeanDefinitionDsl.Role? = null) { - - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + scope: Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: Role? = null, + order: Int? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref()) } } @@ -468,6 +487,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -475,16 +495,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit inline fun bean(crossinline f: (A, B, C, D, E, F) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -504,6 +525,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -512,16 +534,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified G: Any> bean(crossinline f: (A, B, C, D, E, F, G) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -541,6 +564,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -549,16 +573,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified G: Any, reified H: Any> bean(crossinline f: (A, B, C, D, E, F, G, H) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -578,6 +603,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -586,16 +612,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified G: Any, reified H: Any, reified I: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -615,6 +642,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -623,16 +651,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified G: Any, reified H: Any, reified I: Any, reified J: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -652,6 +681,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -660,16 +690,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified G: Any, reified H: Any, reified I: Any, reified J: Any, reified K: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -689,6 +720,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -697,16 +729,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified G: Any, reified H: Any, reified I: Any, reified J: Any, reified K: Any, reified L: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -726,6 +759,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -734,16 +768,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified G: Any, reified H: Any, reified I: Any, reified J: Any, reified K: Any, reified L: Any, reified M: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -763,6 +798,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -772,16 +808,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified N: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -801,6 +838,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -810,16 +848,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified N: Any, reified O: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -839,6 +878,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -848,16 +888,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified N: Any, reified O: Any, reified P: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -877,6 +918,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -886,16 +928,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified N: Any, reified O: Any, reified P: Any, reified Q: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -915,6 +958,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -924,16 +968,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified N: Any, reified O: Any, reified P: Any, reified Q: Any, reified R: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -953,6 +998,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -962,16 +1008,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified N: Any, reified O: Any, reified P: Any, reified Q: Any, reified R: Any, reified S: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -991,6 +1038,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -1000,16 +1048,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified N: Any, reified O: Any, reified P: Any, reified Q: Any, reified R: Any, reified S: Any, reified U: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, U) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -1029,6 +1078,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -1039,16 +1089,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified V: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, U, V) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } @@ -1068,6 +1119,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit * @param destroyMethodName Set the name of the destroy method * @param description Set a human-readable description of this bean definition * @param role Set the role hint for this bean definition + * @param order Set the sort order for the targeted bean * @see GenericApplicationContext.registerBean * @see org.springframework.beans.factory.config.BeanDefinition * @since 5.2 @@ -1078,16 +1130,17 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit reified V: Any, reified W: Any> bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, U, V, W) -> T, name: String? = null, - scope: BeanDefinitionDsl.Scope? = null, + scope: Scope? = null, isLazyInit: Boolean? = null, isPrimary: Boolean? = null, isAutowireCandidate: Boolean? = null, initMethodName: String? = null, destroyMethodName: String? = null, description: String? = null, - role: BeanDefinitionDsl.Role? = null) { + role: Role? = null, + order: Int? = null) { - bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role, order) { f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) } } diff --git a/spring-context/src/main/resources/META-INF/spring/aot.factories b/spring-context/src/main/resources/META-INF/spring/aot.factories index 967e4c22cbaf..2b005a848848 100644 --- a/spring-context/src/main/resources/META-INF/spring/aot.factories +++ b/spring-context/src/main/resources/META-INF/spring/aot.factories @@ -1,3 +1,6 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar= \ +org.springframework.format.support.FormattingConversionServiceRuntimeHints + org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor= \ org.springframework.context.aot.ReflectiveProcessorBeanFactoryInitializationAotProcessor diff --git a/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd index 283e803db813..97e05f62415f 100644 --- a/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd +++ b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd @@ -95,7 +95,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + - + + @@ -183,6 +224,40 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBeanWithBounds.java b/spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java similarity index 78% rename from spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBeanWithBounds.java rename to spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java index 647c071cbfdc..b563b4d37973 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBeanWithBounds.java +++ b/spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.beans.testfixture.beans; +package example.indexed; -public class GenericBeanWithBounds { - - @SafeVarargs - public final void process(T... persons) { - } +/** + * @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 new file mode 100644 index 000000000000..581be8a6f97d --- /dev/null +++ b/spring-context/src/test/java/example/indexed/IndexedJavaxNamedComponent.java @@ -0,0 +1,24 @@ +/* + * 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 ec52b777f92b..6926102be871 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-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.lang.annotation.Target; import org.springframework.context.annotation.Profile; +import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; @Retention(RetentionPolicy.RUNTIME) @@ -32,6 +33,7 @@ String PROFILE_NAME = "dev"; + @AliasFor(annotation = Component.class) String value() default ""; } diff --git a/spring-context/src/test/java/example/scannable/CustomStereotype.java b/spring-context/src/test/java/example/scannable/CustomStereotype.java index 958f7f9af659..c0b3024ecbba 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-2009 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.lang.annotation.Target; import org.springframework.context.annotation.Scope; +import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Service; /** @@ -33,6 +34,7 @@ @Scope("prototype") public @interface CustomStereotype { + @AliasFor(annotation = Service.class) String value() default "thoreau"; } diff --git a/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java b/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java index ffeb6c29e0a6..6140ea0dce36 100644 --- a/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java +++ b/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,6 @@ /** * @author Sam Brannen */ -@jakarta.annotation.ManagedBean +@jakarta.annotation.ManagedBean("myJakartaManagedBeanComponent") public class JakartaManagedBeanComponent { } diff --git a/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java b/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java new file mode 100644 index 000000000000..b3029035d874 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 000000000000..a0fe78e7429a --- /dev/null +++ b/spring-context/src/test/java/example/scannable/JavaxNamedComponent.java @@ -0,0 +1,24 @@ +/* + * 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/org/springframework/aop/aspectj/AfterAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java index 92eecf43e4f3..db6589771595 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +64,7 @@ void setup() throws Exception { } @AfterEach - void tearDown() throws Exception { + void tearDown() { this.ctx.close(); } 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 7efa9b07ce9c..f447f7d78696 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,14 +62,14 @@ void tearDown() { @Test - void simpleAfterThrowing() throws Throwable { + void simpleAfterThrowing() { assertThatExceptionOfType(Throwable.class).isThrownBy(() -> this.testBean.exceptional(new Throwable())); verify(mockCollaborator).noArgs(); } @Test - void afterThrowingWithBinding() throws Throwable { + void afterThrowingWithBinding() { Throwable t = new Throwable(); assertThatExceptionOfType(Throwable.class).isThrownBy(() -> this.testBean.exceptional(t)); @@ -77,7 +77,7 @@ void afterThrowingWithBinding() throws Throwable { } @Test - void afterThrowingWithNamedTypeRestriction() throws Throwable { + void afterThrowingWithNamedTypeRestriction() { Throwable t = new Throwable(); assertThatExceptionOfType(Throwable.class).isThrownBy(() -> this.testBean.exceptional(t)); @@ -87,7 +87,7 @@ void afterThrowingWithNamedTypeRestriction() throws Throwable { } @Test - void afterThrowingWithRuntimeExceptionBinding() throws Throwable { + void afterThrowingWithRuntimeExceptionBinding() { RuntimeException ex = new RuntimeException(); assertThatExceptionOfType(Throwable.class).isThrownBy(() -> this.testBean.exceptional(ex)); @@ -95,14 +95,14 @@ void afterThrowingWithRuntimeExceptionBinding() throws Throwable { } @Test - void afterThrowingWithTypeSpecified() throws Throwable { + void afterThrowingWithTypeSpecified() { assertThatExceptionOfType(Throwable.class).isThrownBy(() -> this.testBean.exceptional(new Throwable())); verify(mockCollaborator).noArgsOnThrowableMatch(); } @Test - void afterThrowingWithRuntimeTypeSpecified() throws Throwable { + void afterThrowingWithRuntimeTypeSpecified() { assertThatExceptionOfType(Throwable.class).isThrownBy(() -> this.testBean.exceptional(new RuntimeException())); verify(mockCollaborator).noArgsOnRuntimeExceptionMatch(); @@ -123,7 +123,7 @@ public interface AfterThrowingAdviceBindingCollaborator { void noArgsOnRuntimeExceptionMatch(); } - protected AfterThrowingAdviceBindingCollaborator collaborator = null; + AfterThrowingAdviceBindingCollaborator collaborator = null; public void setCollaborator(AfterThrowingAdviceBindingCollaborator aCollaborator) { this.collaborator = aCollaborator; 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 e6b065a607e5..5c3bf3814308 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 @@ -38,7 +38,7 @@ * @author Adrian Colyer * @author Chris Beams */ -public class AroundAdviceBindingTests { +class AroundAdviceBindingTests { private AroundAdviceBindingCollaborator mockCollaborator = mock(); @@ -50,7 +50,7 @@ public class AroundAdviceBindingTests { @BeforeEach - public void onSetUp() throws Exception { + void onSetUp() throws Exception { ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); AroundAdviceBindingTestAspect aroundAdviceAspect = (AroundAdviceBindingTestAspect) ctx.getBean("testAspect"); @@ -67,25 +67,25 @@ public void onSetUp() throws Exception { } @Test - public void testOneIntArg() { + void testOneIntArg() { testBeanProxy.setAge(5); verify(mockCollaborator).oneIntArg(5); } @Test - public void testOneObjectArgBoundToTarget() { + void testOneObjectArgBoundToTarget() { testBeanProxy.getAge(); verify(mockCollaborator).oneObjectArg(this.testBeanTarget); } @Test - public void testOneIntAndOneObjectArgs() { + void testOneIntAndOneObjectArgs() { testBeanProxy.setAge(5); verify(mockCollaborator).oneIntAndOneObject(5, this.testBeanProxy); } @Test - public void testJustJoinPoint() { + void testJustJoinPoint() { 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 dab66cca050c..a16529485db7 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,10 +26,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class AroundAdviceCircularTests extends AroundAdviceBindingTests { +class AroundAdviceCircularTests extends AroundAdviceBindingTests { @Test - public void testBothBeansAreProxies() { + void testBothBeansAreProxies() { 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 fddb34c3c688..c7f56212bc4d 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -242,8 +242,7 @@ class SimpleSpringBeforeAdvice implements MethodBeforeAdvice, BeanNameAware { * @see org.springframework.aop.MethodBeforeAdvice#before(java.lang.reflect.Method, java.lang.Object[], java.lang.Object) */ @Override - public void before(Method method, Object[] args, @Nullable Object target) - throws Throwable { + public void before(Method method, Object[] args, @Nullable Object target) { this.collaborator.beforeAdviceOne(this.name); } 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 a56014324e00..ed740ab88ce9 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class BeanNamePointcutTests { +class BeanNamePointcutTests { private ITestBean testBean1; private ITestBean testBean2; @@ -55,7 +55,7 @@ public class BeanNamePointcutTests { @BeforeEach - public void setup() { + void setup() { ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); testBean1 = (ITestBean) ctx.getBean("testBean1"); testBean2 = (ITestBean) ctx.getBean("testBean2"); @@ -74,7 +74,7 @@ public void setup() { // We don't need to test all combination of pointcuts due to BeanNamePointcutMatchingTests @Test - public void testMatchingBeanName() { + void testMatchingBeanName() { boolean condition = this.testBean1 instanceof Advised; assertThat(condition).as("Matching bean must be advised (proxied)").isTrue(); // Call two methods to test for SPR-3953-like condition @@ -84,7 +84,7 @@ public void testMatchingBeanName() { } @Test - public void testNonMatchingBeanName() { + void testNonMatchingBeanName() { boolean condition = this.testBean2 instanceof Advised; assertThat(condition).as("Non-matching bean must *not* be advised (proxied)").isFalse(); this.testBean2.setAge(20); @@ -92,13 +92,13 @@ public void testNonMatchingBeanName() { } @Test - public void testNonMatchingNestedBeanName() { + void testNonMatchingNestedBeanName() { boolean condition = this.testBeanContainingNestedBean.getDoctor() instanceof Advised; assertThat(condition).as("Non-matching bean must *not* be advised (proxied)").isFalse(); } @Test - public void testMatchingFactoryBeanObject() { + void testMatchingFactoryBeanObject() { boolean condition1 = this.testFactoryBean1 instanceof Advised; assertThat(condition1).as("Matching bean must be advised (proxied)").isTrue(); assertThat(this.testFactoryBean1.get("myKey")).isEqualTo("myValue"); @@ -110,7 +110,7 @@ public void testMatchingFactoryBeanObject() { } @Test - public void testMatchingFactoryBeanItself() { + void testMatchingFactoryBeanItself() { boolean condition1 = !(this.testFactoryBean2 instanceof Advised); assertThat(condition1).as("Matching bean must *not* be advised (proxied)").isTrue(); FactoryBean fb = (FactoryBean) ctx.getBean("&testFactoryBean2"); @@ -122,7 +122,7 @@ public void testMatchingFactoryBeanItself() { } @Test - public void testPointcutAdvisorCombination() { + void testPointcutAdvisorCombination() { boolean condition = this.interceptThis instanceof Advised; assertThat(condition).as("Matching bean must be advised (proxied)").isTrue(); boolean condition1 = this.dontInterceptThis instanceof Advised; @@ -139,7 +139,7 @@ public static class TestInterceptor implements MethodBeforeAdvice { private int interceptionCount; @Override - public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + public void before(Method method, Object[] args, @Nullable Object target) { interceptionCount++; } } 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 fc9970bc0916..e1a2b782b946 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,11 +31,10 @@ * @author Ramnivas Laddad * @author Chris Beams */ -public class ImplicitJPArgumentMatchingAtAspectJTests { +class ImplicitJPArgumentMatchingAtAspectJTests { @Test - @SuppressWarnings("resource") - public void testAspect() { + void testAspect() { // 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/OverloadedAdviceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java index 91597c9ab85c..257b6bf37580 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 @@ -28,17 +28,14 @@ * * @author Adrian Colyer * @author Chris Beams + * @author Juergen Hoeller */ class OverloadedAdviceTests { @Test @SuppressWarnings("resource") - void testExceptionOnConfigParsingWithMismatchedAdviceMethod() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass())) - .havingRootCause() - .isInstanceOf(IllegalArgumentException.class) - .as("invalidAbsoluteTypeName should be detected by AJ").withMessageContaining("invalidAbsoluteTypeName"); + void testConfigParsingWithMismatchedAdviceMethod() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); } @Test 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 3293f7568f5d..48f88c9d6473 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,29 +36,25 @@ * @author Juergen Hoeller * @author Chris Beams */ -@SuppressWarnings("resource") -public class PropertyDependentAspectTests { +class PropertyDependentAspectTests { @Test - public void propertyDependentAspectWithPropertyDeclaredBeforeAdvice() - throws Exception { + void propertyDependentAspectWithPropertyDeclaredBeforeAdvice() { checkXmlAspect(getClass().getSimpleName() + "-before.xml"); } @Test - public void propertyDependentAspectWithPropertyDeclaredAfterAdvice() throws Exception { + void propertyDependentAspectWithPropertyDeclaredAfterAdvice() { checkXmlAspect(getClass().getSimpleName() + "-after.xml"); } @Test - public void propertyDependentAtAspectJAspectWithPropertyDeclaredBeforeAdvice() - throws Exception { + void propertyDependentAtAspectJAspectWithPropertyDeclaredBeforeAdvice() { checkAtAspectJAspect(getClass().getSimpleName() + "-atAspectJ-before.xml"); } @Test - public void propertyDependentAtAspectJAspectWithPropertyDeclaredAfterAdvice() - throws Exception { + void propertyDependentAtAspectJAspectWithPropertyDeclaredAfterAdvice() { checkAtAspectJAspect(getClass().getSimpleName() + "-atAspectJ-after.xml"); } @@ -133,7 +129,7 @@ class JoinPointMonitorAtAspectJAspect { int aroundExecutions; @Before("execution(* increment*())") - public void before() { + void before() { beforeExecutions++; } 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 9ae9ec3078c8..c501cc3941bf 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ interface TestInterface { // Reproducing bug requires that the class specified in target() pointcut doesn't // include the advised method's implementation (instead a base class should include it) - static abstract class AbstractTestImpl implements TestInterface { + abstract static class AbstractTestImpl implements TestInterface { @Override public void interfaceMethod() { 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 7bc9ca4de0c9..9a07f3f5e1cc 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,7 +119,7 @@ void atAnnotationMethodAnnotationMatch() { } interface TestInterface { - public void doIt(); + void doIt(); } static class TestImpl implements TestInterface { 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 413929b97723..ae3c314aa1bf 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -107,7 +107,7 @@ void thisAsInterfaceAndTargetAsClassCounterMatch() { interface TestInterface { - public void doIt(); + void doIt(); } 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 6fd601db7bc7..b4bb26d719ad 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-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ */ class AnnotationBindingTestAspect { - public String doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnnotation) throws Throwable { + public String doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnnotation) { return testAnnotation.value(); } 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 21fc29ebdadb..82cd932a2bc1 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ void noMatchingWithoutAnnotationPresent() { class TestMethodInterceptor implements MethodInterceptor { @Override - public Object invoke(MethodInvocation methodInvocation) throws Throwable { + public Object invoke(MethodInvocation methodInvocation) { return "this value"; } } 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 613848e8a3f0..8a3738ba91bd 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ void proxyCreation() { interface AnInterface { - public void interfaceMethod(); + void interfaceMethod(); } 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 69f5f4bd888e..16da2ef070d2 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -220,16 +220,16 @@ void cglibProxyClassIsCachedAcrossApplicationContextsForPerTargetAspect() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { testBean1 = context.getBean(TestBean.class); assertThat(AopUtils.isCglibProxy(testBean1)).as("CGLIB proxy").isTrue(); - assertThat(testBean1.getClass().getInterfaces()) - .containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class); + assertThat(testBean1.getClass().getInterfaces()).containsExactlyInAnyOrder( + Factory.class, SpringProxy.class, Advised.class); } // Round #2 try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { testBean2 = context.getBean(TestBean.class); assertThat(AopUtils.isCglibProxy(testBean2)).as("CGLIB proxy").isTrue(); - assertThat(testBean2.getClass().getInterfaces()) - .containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class); + assertThat(testBean2.getClass().getInterfaces()).containsExactlyInAnyOrder( + Factory.class, SpringProxy.class, Advised.class); } assertThat(testBean1.getClass()).isSameAs(testBean2.getClass()); @@ -344,8 +344,8 @@ void lambdaIsAlwaysProxiedWithJdkProxy(Class configClass) { Supplier supplier = context.getBean(Supplier.class); assertThat(AopUtils.isAopProxy(supplier)).as("AOP proxy").isTrue(); assertThat(AopUtils.isJdkDynamicProxy(supplier)).as("JDK Dynamic proxy").isTrue(); - assertThat(supplier.getClass().getInterfaces()) - .containsExactlyInAnyOrder(Supplier.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + assertThat(supplier.getClass().getInterfaces()).containsExactlyInAnyOrder( + Supplier.class, SpringProxy.class, Advised.class, DecoratingProxy.class); assertThat(supplier.get()).isEqualTo("advised: lambda"); } } @@ -357,26 +357,14 @@ void lambdaIsAlwaysProxiedWithJdkProxyWithIntroductions(Class configClass) { MessageGenerator messageGenerator = context.getBean(MessageGenerator.class); assertThat(AopUtils.isAopProxy(messageGenerator)).as("AOP proxy").isTrue(); assertThat(AopUtils.isJdkDynamicProxy(messageGenerator)).as("JDK Dynamic proxy").isTrue(); - assertThat(messageGenerator.getClass().getInterfaces()) - .containsExactlyInAnyOrder(MessageGenerator.class, Mixin.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + assertThat(messageGenerator.getClass().getInterfaces()).containsExactlyInAnyOrder( + MessageGenerator.class, Mixin.class, SpringProxy.class, Advised.class, DecoratingProxy.class); assertThat(messageGenerator.generateMessage()).isEqualTo("mixin: lambda"); } } - /** - * Returns a new {@link ClassPathXmlApplicationContext} for the file ending in fileSuffix. - */ private ClassPathXmlApplicationContext newContext(String fileSuffix) { - return new ClassPathXmlApplicationContext(qName(fileSuffix), getClass()); - } - - /** - * Returns the relatively qualified name for fileSuffix. - * e.g. for a fileSuffix='foo.xml', this method will return - * 'AspectJAutoProxyCreatorTests-foo.xml' - */ - private String qName(String fileSuffix) { - return String.format("%s-%s", getClass().getSimpleName(), fileSuffix); + return new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-" + fileSuffix, getClass()); } } @@ -416,7 +404,6 @@ class DummyAspectWithParameter { public Object test(ProceedingJoinPoint pjp, int age) throws Throwable { return pjp.proceed(); } - } class DummyFactoryBean implements FactoryBean { @@ -435,7 +422,6 @@ public Class getObjectType() { public boolean isSingleton() { throw new UnsupportedOperationException(); } - } @Aspect @@ -591,7 +577,6 @@ public int unreliable() { } return this.calls; } - } @SuppressWarnings("serial") @@ -607,7 +592,6 @@ public TestBeanAdvisor() { public boolean matches(Method method, @Nullable Class targetClass) { return ITestBean.class.isAssignableFrom(targetClass); } - } abstract class AbstractProxyTargetClassConfig { @@ -625,7 +609,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(); } @@ -661,6 +645,7 @@ PerTargetAspect perTargetAspect() { @FunctionalInterface interface MessageGenerator { + String generateMessage(); } @@ -678,7 +663,6 @@ public Object invoke(MethodInvocation invocation) throws Throwable { public boolean implementsInterface(Class intf) { return Mixin.class.isAssignableFrom(intf); } - } @SuppressWarnings("serial") @@ -708,7 +692,6 @@ public ClassFilter getClassFilter() { public void validateInterfaces() { /* no-op */ } - } abstract class AbstractMixinConfig { @@ -722,7 +705,6 @@ MessageGenerator messageGenerator() { MixinAdvisor mixinAdvisor() { return new MixinAdvisor(); } - } @Configuration(proxyBeanMethods = false) 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 a3758b73b81e..ae6c1c7b3b1e 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class AtAspectJAnnotationBindingTests { +class AtAspectJAnnotationBindingTests { private AnnotatedTestBean testBean; @@ -41,26 +41,26 @@ public class AtAspectJAnnotationBindingTests { @BeforeEach - public void setup() { + void setup() { ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); testBean = (AnnotatedTestBean) ctx.getBean("testBean"); } @Test - public void testAnnotationBindingInAroundAdvice() { + void testAnnotationBindingInAroundAdvice() { assertThat(testBean.doThis()).isEqualTo("this value doThis"); assertThat(testBean.doThat()).isEqualTo("that value doThat"); assertThat(testBean.doArray()).hasSize(2); } @Test - public void testNoMatchingWithoutAnnotationPresent() { + void testNoMatchingWithoutAnnotationPresent() { assertThat(testBean.doTheOther()).isEqualTo("doTheOther"); } @Test - public void testPointcutEvaluatedAgainstArray() { + void testPointcutEvaluatedAgainstArray() { ctx.getBean("arrayFactoryBean"); } 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 df5d587e6a5c..69793e8ee3c7 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -117,7 +117,7 @@ private long testRepeatedAroundAdviceInvocations(String file, int howmany, Strin sw.stop(); // System.out.println(sw.prettyPrint()); ac.close(); - return sw.getLastTaskTimeMillis(); + return sw.getTotalTimeMillis(); } private long testBeforeAdviceWithoutJoinPoint(String file, int howmany, String technology) { @@ -139,7 +139,7 @@ private long testBeforeAdviceWithoutJoinPoint(String file, int howmany, String t sw.stop(); // System.out.println(sw.prettyPrint()); ac.close(); - return sw.getLastTaskTimeMillis(); + return sw.getTotalTimeMillis(); } private long testAfterReturningAdviceWithoutJoinPoint(String file, int howmany, String technology) { @@ -162,7 +162,7 @@ private long testAfterReturningAdviceWithoutJoinPoint(String file, int howmany, sw.stop(); // System.out.println(sw.prettyPrint()); ac.close(); - return sw.getLastTaskTimeMillis(); + return sw.getTotalTimeMillis(); } private long testMix(String file, int howmany, String technology) { @@ -191,7 +191,7 @@ private long testMix(String file, int howmany, String technology) { sw.stop(); // System.out.println(sw.prettyPrint()); ac.close(); - return sw.getLastTaskTimeMillis(); + return sw.getTotalTimeMillis(); } } @@ -226,7 +226,7 @@ class TraceAfterReturningAdvice implements AfterReturningAdvice { public int afterTakesInt; @Override - public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { + public void afterReturning(Object returnValue, Method method, Object[] args, Object target) { ++afterTakesInt; } @@ -270,7 +270,7 @@ class TraceBeforeAdvice implements MethodBeforeAdvice { public int beforeStringReturn; @Override - public void before(Method method, Object[] args, Object target) throws Throwable { + public void before(Method method, Object[] args, Object target) { ++beforeStringReturn; } 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 cf71cca93440..9da8d7ed3b0a 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ void serviceIsAdvised() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); - Service service = ctx.getBean(Service.class); + Service service = ctx.getBean(Service.class); assertThatRuntimeException() .isThrownBy(service::serveMe) .withMessage("advice invoked"); @@ -59,7 +59,7 @@ void serviceIsAdvised() { class TransactionInterceptor { @Around(value="execution(* *..Service.*(..)) && @annotation(transaction)") - public Object around(ProceedingJoinPoint pjp, Transaction transaction) throws Throwable { + public Object around(ProceedingJoinPoint pjp, Transaction transaction) { throw new RuntimeException("advice invoked"); //return pjp.proceed(); } 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 5e1c2d7b3313..b74ffec0f1a2 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,15 @@ * @author Adrian Colyer * @author Chris Beams */ -public class AopNamespaceHandlerAdviceTypeTests { +class AopNamespaceHandlerAdviceTypeTests { @Test - @SuppressWarnings("resource") - public void testParsingOfAdviceTypes() { + void testParsingOfAdviceTypes() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - public void testParsingOfAdviceTypesWithError() { + void testParsingOfAdviceTypesWithError() { 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 cafedebb2a40..946fb953a458 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,16 +27,15 @@ * @author Adrian Colyer * @author Chris Beams */ -public class AopNamespaceHandlerArgNamesTests { +class AopNamespaceHandlerArgNamesTests { @Test - @SuppressWarnings("resource") - public void testArgNamesOK() { + void testArgNamesOK() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - public void testArgNamesError() { + void testArgNamesError() { 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 d295b1d1edc7..aa7fba9edad3 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ * @author Rob Harrop * @author Chris Beams */ -public class AopNamespaceHandlerProxyTargetClassTests extends AopNamespaceHandlerTests { +class AopNamespaceHandlerProxyTargetClassTests extends AopNamespaceHandlerTests { @Test - public void testIsClassProxy() { + void testIsClassProxy() { 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 b260df120fbc..58b5b2a67de3 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,15 @@ * @author Adrian Colyer * @author Chris Beams */ -public class AopNamespaceHandlerReturningTests { +class AopNamespaceHandlerReturningTests { @Test - @SuppressWarnings("resource") - public void testReturningOnReturningAdvice() { + void testReturningOnReturningAdvice() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - public void testParseReturningOnOtherAdviceType() { + void testParseReturningOnOtherAdviceType() { 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 4f6faf2d26a2..4ee987365c5d 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,18 +32,18 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for aop namespace. + * Tests for {@link AopNamespaceHandler}. * * @author Rob Harrop * @author Chris Beams */ -public class AopNamespaceHandlerTests { +class AopNamespaceHandlerTests { private ApplicationContext context; @BeforeEach - public void setup() { + void setup() { this.context = new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); } @@ -53,7 +53,7 @@ protected ITestBean getTestBean() { @Test - public void testIsProxy() throws Exception { + void testIsProxy() { ITestBean bean = getTestBean(); assertThat(AopUtils.isAopProxy(bean)).as("Bean is not a proxy").isTrue(); @@ -66,7 +66,7 @@ public void testIsProxy() throws Exception { } @Test - public void testAdviceInvokedCorrectly() throws Exception { + void testAdviceInvokedCorrectly() { CountingBeforeAdvice getAgeCounter = (CountingBeforeAdvice) this.context.getBean("getAgeCounter"); CountingBeforeAdvice getNameCounter = (CountingBeforeAdvice) this.context.getBean("getNameCounter"); @@ -87,7 +87,7 @@ public void testAdviceInvokedCorrectly() throws Exception { } @Test - public void testAspectApplied() throws Exception { + void testAspectApplied() { ITestBean bean = getTestBean(); CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); @@ -107,7 +107,7 @@ public void testAspectApplied() throws Exception { } @Test - public void testAspectAppliedForInitializeBeanWithEmptyName() { + void testAspectAppliedForInitializeBeanWithEmptyName() { ITestBean bean = (ITestBean) this.context.getAutowireCapableBeanFactory().initializeBean(new TestBean(), ""); CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); @@ -127,7 +127,7 @@ public void testAspectAppliedForInitializeBeanWithEmptyName() { } @Test - public void testAspectAppliedForInitializeBeanWithNullName() { + void testAspectAppliedForInitializeBeanWithNullName() { ITestBean bean = (ITestBean) this.context.getAutowireCapableBeanFactory().initializeBean(new TestBean(), null); CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); @@ -157,11 +157,11 @@ class CountingAspectJAdvice { private int aroundCount; - public void myBeforeAdvice() throws Throwable { + public void myBeforeAdvice() { this.beforeCount++; } - public void myAfterAdvice() throws Throwable { + public void myAfterAdvice() { this.afterCount++; } 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 3a67a8bc2628..4b79307f76bd 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,15 @@ * @author Adrian Colyer * @author Chris Beams */ -public class AopNamespaceHandlerThrowingTests { +class AopNamespaceHandlerThrowingTests { @Test - @SuppressWarnings("resource") - public void testThrowingOnThrowingAdvice() { + void testThrowingOnThrowingAdvice() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - public void testParseThrowingOnOtherAdviceType() { + void testParseThrowingOnOtherAdviceType() { 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 99904f649ea2..c3f7af3662ee 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 @@ -32,7 +32,7 @@ * @author Rick Evans * @author Chris Beams */ -public class MethodLocatingFactoryBeanTests { +class MethodLocatingFactoryBeanTests { private static final String BEAN_NAME = "string"; private MethodLocatingFactoryBean factory = new MethodLocatingFactoryBean(); @@ -40,24 +40,24 @@ public class MethodLocatingFactoryBeanTests { @Test - public void testIsSingleton() { + void testIsSingleton() { assertThat(factory.isSingleton()).isTrue(); } @Test - public void testGetObjectType() { + void testGetObjectType() { assertThat(factory.getObjectType()).isEqualTo(Method.class); } @Test - public void testWithNullTargetBeanName() { + void testWithNullTargetBeanName() { factory.setMethodName("toString()"); assertThatIllegalArgumentException().isThrownBy(() -> factory.setBeanFactory(beanFactory)); } @Test - public void testWithEmptyTargetBeanName() { + void testWithEmptyTargetBeanName() { factory.setTargetBeanName(""); factory.setMethodName("toString()"); assertThatIllegalArgumentException().isThrownBy(() -> @@ -65,14 +65,14 @@ public void testWithEmptyTargetBeanName() { } @Test - public void testWithNullTargetMethodName() { + void testWithNullTargetMethodName() { factory.setTargetBeanName(BEAN_NAME); assertThatIllegalArgumentException().isThrownBy(() -> factory.setBeanFactory(beanFactory)); } @Test - public void testWithEmptyTargetMethodName() { + void testWithEmptyTargetMethodName() { factory.setTargetBeanName(BEAN_NAME); factory.setMethodName(""); assertThatIllegalArgumentException().isThrownBy(() -> @@ -80,7 +80,7 @@ public void testWithEmptyTargetMethodName() { } @Test - public void testWhenTargetBeanClassCannotBeResolved() { + void testWhenTargetBeanClassCannotBeResolved() { factory.setTargetBeanName(BEAN_NAME); factory.setMethodName("toString()"); assertThatIllegalArgumentException().isThrownBy(() -> @@ -90,7 +90,7 @@ public void testWhenTargetBeanClassCannotBeResolved() { @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - public void testSunnyDayPath() throws Exception { + void testSunnyDayPath() throws Exception { given(beanFactory.getType(BEAN_NAME)).willReturn((Class)String.class); factory.setTargetBeanName(BEAN_NAME); factory.setMethodName("toString()"); @@ -105,7 +105,7 @@ public void testSunnyDayPath() throws Exception { @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - public void testWhereMethodCannotBeResolved() { + void testWhereMethodCannotBeResolved() { 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/framework/AbstractAopProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java index 9460d8c969c6..75b564c2d308 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -97,12 +96,12 @@ public abstract class AbstractAopProxyTests { * to ensure that it was used appropriately by code. */ @BeforeEach - public void reset() { + void reset() { mockTargetSource.reset(); } @AfterEach - public void verify() { + void verify() { mockTargetSource.verify(); } @@ -126,7 +125,7 @@ protected boolean requiresTarget() { * Simple test that if we set values we can get them out again. */ @Test - public void testValuesStick() { + void testValuesStick() { int age1 = 33; int age2 = 37; String name = "tony"; @@ -147,7 +146,7 @@ public void testValuesStick() { } @Test - public void testSerializationAdviceAndTargetNotSerializable() throws Exception { + void testSerializationAdviceAndTargetNotSerializable() throws Exception { TestBean tb = new TestBean(); assertThat(SerializationTestUtils.isSerializable(tb)).isFalse(); @@ -160,7 +159,7 @@ public void testSerializationAdviceAndTargetNotSerializable() throws Exception { } @Test - public void testSerializationAdviceNotSerializable() throws Exception { + void testSerializationAdviceNotSerializable() throws Exception { SerializablePerson sp = new SerializablePerson(); assertThat(SerializationTestUtils.isSerializable(sp)).isTrue(); @@ -176,7 +175,7 @@ public void testSerializationAdviceNotSerializable() throws Exception { } @Test - public void testSerializableTargetAndAdvice() throws Throwable { + void testSerializableTargetAndAdvice() throws Throwable { SerializablePerson personTarget = new SerializablePerson(); personTarget.setName("jim"); personTarget.setAge(26); @@ -246,7 +245,7 @@ public void testSerializableTargetAndAdvice() throws Throwable { * Check also proxy exposure. */ @Test - public void testOneAdvisedObjectCallsAnother() { + void testOneAdvisedObjectCallsAnother() { int age1 = 33; int age2 = 37; @@ -291,7 +290,7 @@ public void testOneAdvisedObjectCallsAnother() { @Test - public void testReentrance() { + void testReentrance() { int age1 = 33; TestBean target1 = new TestBean(); @@ -315,7 +314,7 @@ public void testReentrance() { } @Test - public void testTargetCanGetProxy() { + void testTargetCanGetProxy() { NopInterceptor di = new NopInterceptor(); INeedsToSeeProxy target = new TargetChecker(); ProxyFactory proxyFactory = new ProxyFactory(target); @@ -344,24 +343,23 @@ public void testTargetCantGetProxyByDefault() { ProxyFactory pf1 = new ProxyFactory(et); assertThat(pf1.isExposeProxy()).isFalse(); INeedsToSeeProxy proxied = (INeedsToSeeProxy) createProxy(pf1); - assertThatIllegalStateException().isThrownBy(() -> - proxied.incrementViaProxy()); + assertThatIllegalStateException().isThrownBy(proxied::incrementViaProxy); } @Test - public void testContext() throws Throwable { + void testContext() { testContext(true); } @Test - public void testNoContext() throws Throwable { + void testNoContext() { testContext(false); } /** * @param context if true, want context */ - private void testContext(final boolean context) throws Throwable { + private void testContext(final boolean context) { final String s = "foo"; // Test return value MethodInterceptor mi = invocation -> { @@ -395,7 +393,7 @@ private void testContext(final boolean context) throws Throwable { * target returns {@code this} */ @Test - public void testTargetReturnsThis() throws Throwable { + void testTargetReturnsThis() { // Test return value TestBean raw = new OwnSpouse(); @@ -408,7 +406,7 @@ public void testTargetReturnsThis() throws Throwable { } @Test - public void testDeclaredException() throws Throwable { + void testDeclaredException() { final Exception expectedException = new Exception(); // Test return value MethodInterceptor mi = invocation -> { @@ -436,7 +434,7 @@ public void testDeclaredException() throws Throwable { * org.springframework.cglib UndeclaredThrowableException */ @Test - public void testUndeclaredCheckedException() throws Throwable { + void testUndeclaredCheckedException() { final Exception unexpectedException = new Exception(); // Test return value MethodInterceptor mi = invocation -> { @@ -456,7 +454,7 @@ public void testUndeclaredCheckedException() throws Throwable { } @Test - public void testUndeclaredUncheckedException() throws Throwable { + void testUndeclaredUncheckedException() { final RuntimeException unexpectedException = new RuntimeException(); // Test return value MethodInterceptor mi = invocation -> { @@ -482,7 +480,7 @@ public void testUndeclaredUncheckedException() throws Throwable { * so as to guarantee a consistent programming model. */ @Test - public void testTargetCanGetInvocationEvenIfNoAdviceChain() throws Throwable { + void testTargetCanGetInvocationEvenIfNoAdviceChain() { NeedsToSeeProxy target = new NeedsToSeeProxy(); AdvisedSupport pc = new AdvisedSupport(INeedsToSeeProxy.class); pc.setTarget(target); @@ -496,7 +494,7 @@ public void testTargetCanGetInvocationEvenIfNoAdviceChain() throws Throwable { } @Test - public void testTargetCanGetInvocation() throws Throwable { + void testTargetCanGetInvocation() { final InvocationCheckExposedInvocationTestBean expectedTarget = new InvocationCheckExposedInvocationTestBean(); AdvisedSupport pc = new AdvisedSupport(ITestBean.class, IOther.class); @@ -529,7 +527,7 @@ private void assertNoInvocationContext() { * Test stateful interceptor */ @Test - public void testMixinWithIntroductionAdvisor() throws Throwable { + void testMixinWithIntroductionAdvisor() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -540,7 +538,7 @@ public void testMixinWithIntroductionAdvisor() throws Throwable { } @Test - public void testMixinWithIntroductionInfo() throws Throwable { + void testMixinWithIntroductionInfo() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -573,7 +571,7 @@ private void testTestBeanIntroduction(ProxyFactory pc) { } @Test - public void testReplaceArgument() throws Throwable { + void testReplaceArgument() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -594,7 +592,7 @@ public void testReplaceArgument() throws Throwable { } @Test - public void testCanCastProxyToProxyConfig() throws Throwable { + void testCanCastProxyToProxyConfig() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(tb); NopInterceptor di = new NopInterceptor(); @@ -630,7 +628,7 @@ public void testCanCastProxyToProxyConfig() throws Throwable { } @Test - public void testAdviceImplementsIntroductionInfo() throws Throwable { + void testAdviceImplementsIntroductionInfo() { TestBean tb = new TestBean(); String name = "tony"; tb.setName(name); @@ -647,7 +645,7 @@ public void testAdviceImplementsIntroductionInfo() throws Throwable { } @Test - public void testCannotAddDynamicIntroductionAdviceExceptInIntroductionAdvice() throws Throwable { + void testCannotAddDynamicIntroductionAdviceExceptInIntroductionAdvice() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -660,7 +658,7 @@ public void testCannotAddDynamicIntroductionAdviceExceptInIntroductionAdvice() t } @Test - public void testRejectsBogusDynamicIntroductionAdviceWithNoAdapter() throws Throwable { + void testRejectsBogusDynamicIntroductionAdviceWithNoAdapter() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -681,7 +679,7 @@ public void testRejectsBogusDynamicIntroductionAdviceWithNoAdapter() throws Thro * that are unsupported by the IntroductionInterceptor. */ @Test - public void testCannotAddIntroductionAdviceWithUnimplementedInterface() throws Throwable { + void testCannotAddIntroductionAdviceWithUnimplementedInterface() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -697,7 +695,7 @@ public void testCannotAddIntroductionAdviceWithUnimplementedInterface() throws T * as it's constrained by the interface. */ @Test - public void testIntroductionThrowsUncheckedException() throws Throwable { + void testIntroductionThrowsUncheckedException() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -722,7 +720,7 @@ public long getTimeStamp() { * Should only be able to introduce interfaces, not classes. */ @Test - public void testCannotAddIntroductionAdviceToIntroduceClass() throws Throwable { + void testCannotAddIntroductionAdviceToIntroduceClass() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -735,7 +733,7 @@ public void testCannotAddIntroductionAdviceToIntroduceClass() throws Throwable { } @Test - public void testCannotAddInterceptorWhenFrozen() throws Throwable { + void testCannotAddInterceptorWhenFrozen() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -755,7 +753,7 @@ public void testCannotAddInterceptorWhenFrozen() throws Throwable { * Check that casting to Advised can't get around advice freeze. */ @Test - public void testCannotAddAdvisorWhenFrozenUsingCast() throws Throwable { + void testCannotAddAdvisorWhenFrozenUsingCast() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -775,7 +773,7 @@ public void testCannotAddAdvisorWhenFrozenUsingCast() throws Throwable { } @Test - public void testCannotRemoveAdvisorWhenFrozen() throws Throwable { + void testCannotRemoveAdvisorWhenFrozen() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -800,7 +798,7 @@ public void testCannotRemoveAdvisorWhenFrozen() throws Throwable { } @Test - public void testUseAsHashKey() { + void testUseAsHashKey() { TestBean target1 = new TestBean(); ProxyFactory pf1 = new ProxyFactory(target1); pf1.addAdvice(new NopInterceptor()); @@ -825,7 +823,7 @@ public void testUseAsHashKey() { * Check that the string is informative. */ @Test - public void testProxyConfigString() { + void testProxyConfigString() { TestBean target = new TestBean(); ProxyFactory pc = new ProxyFactory(target); pc.setInterfaces(ITestBean.class); @@ -841,7 +839,7 @@ public void testProxyConfigString() { } @Test - public void testCanPreventCastToAdvisedUsingOpaque() { + void testCanPreventCastToAdvisedUsingOpaque() { TestBean target = new TestBean(); ProxyFactory pc = new ProxyFactory(target); pc.setInterfaces(ITestBean.class); @@ -862,7 +860,7 @@ public void testCanPreventCastToAdvisedUsingOpaque() { } @Test - public void testAdviceSupportListeners() throws Throwable { + void testAdviceSupportListeners() { TestBean target = new TestBean(); target.setAge(21); @@ -901,7 +899,7 @@ public void testAdviceSupportListeners() throws Throwable { } @Test - public void testExistingProxyChangesTarget() throws Throwable { + void testExistingProxyChangesTarget() { TestBean tb1 = new TestBean(); tb1.setAge(33); @@ -944,7 +942,7 @@ public void testExistingProxyChangesTarget() throws Throwable { } @Test - public void testDynamicMethodPointcutThatAlwaysAppliesStatically() throws Throwable { + void testDynamicMethodPointcutThatAlwaysAppliesStatically() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -961,7 +959,7 @@ public void testDynamicMethodPointcutThatAlwaysAppliesStatically() throws Throwa } @Test - public void testDynamicMethodPointcutThatAppliesStaticallyOnlyToSetters() throws Throwable { + void testDynamicMethodPointcutThatAppliesStaticallyOnlyToSetters() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -984,7 +982,7 @@ public void testDynamicMethodPointcutThatAppliesStaticallyOnlyToSetters() throws } @Test - public void testStaticMethodPointcut() throws Throwable { + void testStaticMethodPointcut() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -1006,7 +1004,7 @@ public void testStaticMethodPointcut() throws Throwable { * We can do this if we clone the invocation. */ @Test - public void testCloneInvocationToProceedThreeTimes() throws Throwable { + void testCloneInvocationToProceedThreeTimes() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(tb); pc.addInterface(ITestBean.class); @@ -1043,14 +1041,11 @@ public boolean matches(Method m, @Nullable Class targetClass) { * We want to change the arguments on a clone: it shouldn't affect the original. */ @Test - public void testCanChangeArgumentsIndependentlyOnClonedInvocation() throws Throwable { + void testCanChangeArgumentsIndependentlyOnClonedInvocation() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(tb); pc.addInterface(ITestBean.class); - /** - * Changes the name, then changes it back. - */ MethodInterceptor nameReverter = mi -> { MethodInvocation clone = ((ReflectiveMethodInvocation) mi).invocableClone(); String oldName = ((ITestBean) mi.getThis()).getName(); @@ -1085,14 +1080,12 @@ public Object invoke(MethodInvocation mi) throws Throwable { it.setName(name2); // NameReverter saved it back assertThat(it.getName()).isEqualTo(name1); - assertThat(saver.names).hasSize(2); - assertThat(saver.names.get(0)).isEqualTo(name2); - assertThat(saver.names.get(1)).isEqualTo(name1); + assertThat(saver.names).containsExactly(name2, name1); } @SuppressWarnings("serial") @Test - public void testOverloadedMethodsWithDifferentAdvice() throws Throwable { + void testOverloadedMethodsWithDifferentAdvice() { Overloads target = new Overloads(); ProxyFactory pc = new ProxyFactory(target); @@ -1128,7 +1121,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - public void testProxyIsBoundBeforeTargetSourceInvoked() { + void testProxyIsBoundBeforeTargetSourceInvoked() { final TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); pf.addAdvice(new DebugInterceptor()); @@ -1143,17 +1136,10 @@ public Class getTargetClass() { return TestBean.class; } @Override - public boolean isStatic() { - return false; - } - @Override - public Object getTarget() throws Exception { + public Object getTarget() { assertThat(AopContext.currentProxy()).isEqualTo(proxy); return target; } - @Override - public void releaseTarget(Object target) throws Exception { - } }); // Just test anything: it will fail if context wasn't found @@ -1161,7 +1147,7 @@ public void releaseTarget(Object target) throws Exception { } @Test - public void testEquals() { + void testEquals() { IOther a = new AllInstancesAreEqual(); IOther b = new AllInstancesAreEqual(); NopInterceptor i1 = new NopInterceptor(); @@ -1178,7 +1164,7 @@ public void testEquals() { assertThat(i2).isEqualTo(i1); assertThat(proxyB).isEqualTo(proxyA); assertThat(proxyB.hashCode()).isEqualTo(proxyA.hashCode()); - assertThat(proxyA.equals(a)).isFalse(); + assertThat(proxyA).isNotEqualTo(a); // Equality checks were handled by the proxy assertThat(i1.getCount()).isEqualTo(0); @@ -1187,11 +1173,11 @@ public void testEquals() { // and won't think it's equal to B's NopInterceptor proxyA.absquatulate(); assertThat(i1.getCount()).isEqualTo(1); - assertThat(proxyA.equals(proxyB)).isFalse(); + assertThat(proxyA).isNotEqualTo(proxyB); } @Test - public void testBeforeAdvisorIsInvoked() { + void testBeforeAdvisorIsInvoked() { CountingBeforeAdvice cba = new CountingBeforeAdvice(); @SuppressWarnings("serial") Advisor matchesNoArgs = new StaticMethodMatcherPointcutAdvisor(cba) { @@ -1220,7 +1206,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - public void testUserAttributes() throws Throwable { + void testUserAttributes() { class MapAwareMethodInterceptor implements MethodInterceptor { private final Map expectedValues; private final Map valuesToAdd; @@ -1231,8 +1217,7 @@ public MapAwareMethodInterceptor(Map expectedValues, Map it = rmi.getUserAttributes().keySet().iterator(); it.hasNext(); ){ - Object key = it.next(); + for (Object key : rmi.getUserAttributes().keySet()) { assertThat(rmi.getUserAttributes().get(key)).isEqualTo(expectedValues.get(key)); } rmi.getUserAttributes().putAll(valuesToAdd); @@ -1240,7 +1225,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } } AdvisedSupport pc = new AdvisedSupport(ITestBean.class); - MapAwareMethodInterceptor mami1 = new MapAwareMethodInterceptor(new HashMap<>(), new HashMap()); + MapAwareMethodInterceptor mami1 = new MapAwareMethodInterceptor(new HashMap<>(), new HashMap<>()); Map firstValuesToAdd = new HashMap<>(); firstValuesToAdd.put("test", ""); MapAwareMethodInterceptor mami2 = new MapAwareMethodInterceptor(new HashMap<>(), firstValuesToAdd); @@ -1272,7 +1257,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } @Test - public void testMultiAdvice() throws Throwable { + void testMultiAdvice() { CountingMultiAdvice cca = new CountingMultiAdvice(); @SuppressWarnings("serial") Advisor matchesNoArgs = new StaticMethodMatcherPointcutAdvisor(cca) { @@ -1306,7 +1291,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - public void testBeforeAdviceThrowsException() { + void testBeforeAdviceThrowsException() { final RuntimeException rex = new RuntimeException(); @SuppressWarnings("serial") CountingBeforeAdvice ba = new CountingBeforeAdvice() { @@ -1348,11 +1333,11 @@ public void before(Method m, Object[] args, Object target) throws Throwable { @Test - public void testAfterReturningAdvisorIsInvoked() { + void testAfterReturningAdvisorIsInvoked() { class SummingAfterAdvice implements AfterReturningAdvice { public int sum; @Override - public void afterReturning(@Nullable Object returnValue, Method m, Object[] args, @Nullable Object target) throws Throwable { + public void afterReturning(@Nullable Object returnValue, Method m, Object[] args, @Nullable Object target) { sum += (Integer) returnValue; } } @@ -1385,7 +1370,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - public void testAfterReturningAdvisorIsNotInvokedOnException() { + void testAfterReturningAdvisorIsNotInvokedOnException() { CountingAfterReturningAdvice car = new CountingAfterReturningAdvice(); TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); @@ -1407,7 +1392,7 @@ public void testAfterReturningAdvisorIsNotInvokedOnException() { @Test - public void testThrowsAdvisorIsInvoked() throws Throwable { + void testThrowsAdvisorIsInvoked() { // Reacts to ServletException and RemoteException MyThrowsHandler th = new MyThrowsHandler(); @SuppressWarnings("serial") @@ -1440,7 +1425,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - public void testAddThrowsAdviceWithoutAdvisor() throws Throwable { + void testAddThrowsAdviceWithoutAdvisor() { // Reacts to ServletException and RemoteException MyThrowsHandler th = new MyThrowsHandler(); @@ -1799,21 +1784,20 @@ public static class CountingMultiAdvice extends MethodCounter implements MethodB AfterReturningAdvice, ThrowsAdvice { @Override - public void before(Method m, Object[] args, @Nullable Object target) throws Throwable { + public void before(Method m, Object[] args, @Nullable Object target) { count(m); } @Override - public void afterReturning(@Nullable Object o, Method m, Object[] args, @Nullable Object target) - throws Throwable { + public void afterReturning(@Nullable Object o, Method m, Object[] args, @Nullable Object target) { count(m); } - public void afterThrowing(IOException ex) throws Throwable { + public void afterThrowing(IOException ex) { count(IOException.class.getName()); } - public void afterThrowing(UncheckedException ex) throws Throwable { + public void afterThrowing(UncheckedException ex) { count(UncheckedException.class.getName()); } @@ -1823,11 +1807,11 @@ public void afterThrowing(UncheckedException ex) throws Throwable { @SuppressWarnings("serial") public static class CountingThrowsAdvice extends MethodCounter implements ThrowsAdvice { - public void afterThrowing(IOException ex) throws Throwable { + public void afterThrowing(IOException ex) { count(IOException.class.getName()); } - public void afterThrowing(UncheckedException ex) throws Throwable { + public void afterThrowing(UncheckedException ex) { count(UncheckedException.class.getName()); } @@ -1878,7 +1862,7 @@ public Class getTargetClass() { * @see org.springframework.aop.TargetSource#getTarget() */ @Override - public Object getTarget() throws Exception { + public Object getTarget() { ++gets; return target; } @@ -1887,7 +1871,7 @@ public Object getTarget() throws Exception { * @see org.springframework.aop.TargetSource#releaseTarget(java.lang.Object) */ @Override - public void releaseTarget(Object pTarget) throws Exception { + public void releaseTarget(Object pTarget) { if (pTarget != this.target) { throw new RuntimeException("Released wrong target"); } @@ -1903,19 +1887,10 @@ public void verify() { throw new RuntimeException("Expectation failed: " + gets + " gets and " + releases + " releases"); } } - - /** - * @see org.springframework.aop.TargetSource#isStatic() - */ - @Override - public boolean isStatic() { - return false; - } - } - static abstract class ExposedInvocationTestBean extends TestBean { + abstract static class ExposedInvocationTestBean extends TestBean { @Override public String getName() { 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 1c99d3fb0e76..cf466b2095bf 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ import org.springframework.aop.testfixture.mixin.LockMixinAdvisor; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextException; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.lang.NonNull; @@ -52,8 +51,7 @@ * @author Ramnivas Laddad * @author Chris Beams */ -@SuppressWarnings("serial") -public class CglibProxyTests extends AbstractAopProxyTests implements Serializable { +class CglibProxyTests extends AbstractAopProxyTests { private static final String DEPENDENCY_CHECK_CONTEXT = CglibProxyTests.class.getSimpleName() + "-with-dependency-checking.xml"; @@ -80,13 +78,13 @@ protected boolean requiresTarget() { @Test - public void testNullConfig() { + void testNullConfig() { assertThatIllegalArgumentException().isThrownBy(() -> new CglibAopProxy(null)); } @Test - public void testNoTarget() { + void testNoTarget() { AdvisedSupport pc = new AdvisedSupport(ITestBean.class); pc.addAdvice(new NopInterceptor()); AopProxy aop = createAopProxy(pc); @@ -94,7 +92,7 @@ public void testNoTarget() { } @Test - public void testProtectedMethodInvocation() { + void testProtectedMethodInvocation() { ProtectedMethodTestBean bean = new ProtectedMethodTestBean(); bean.value = "foo"; mockTargetSource.setTarget(bean); @@ -111,7 +109,7 @@ public void testProtectedMethodInvocation() { } @Test - public void testPackageMethodInvocation() { + void testPackageMethodInvocation() { PackageMethodTestBean bean = new PackageMethodTestBean(); bean.value = "foo"; mockTargetSource.setTarget(bean); @@ -128,7 +126,7 @@ public void testPackageMethodInvocation() { } @Test - public void testProxyCanBeClassNotInterface() { + void testProxyCanBeClassNotInterface() { TestBean raw = new TestBean(); raw.setAge(32); mockTargetSource.setTarget(raw); @@ -146,7 +144,7 @@ public void testProxyCanBeClassNotInterface() { } @Test - public void testMethodInvocationDuringConstructor() { + void testMethodInvocationDuringConstructor() { CglibTestBean bean = new CglibTestBean(); bean.setName("Rob Harrop"); @@ -160,7 +158,7 @@ public void testMethodInvocationDuringConstructor() { } @Test - public void testToStringInvocation() { + void testToStringInvocation() { PrivateCglibTestBean bean = new PrivateCglibTestBean(); bean.setName("Rob Harrop"); @@ -174,7 +172,7 @@ public void testToStringInvocation() { } @Test - public void testUnadvisedProxyCreationWithCallDuringConstructor() { + void testUnadvisedProxyCreationWithCallDuringConstructor() { CglibTestBean target = new CglibTestBean(); target.setName("Rob Harrop"); @@ -189,7 +187,7 @@ public void testUnadvisedProxyCreationWithCallDuringConstructor() { } @Test - public void testMultipleProxies() { + void testMultipleProxies() { TestBean target = new TestBean(); target.setAge(20); TestBean target2 = new TestBean(); @@ -235,7 +233,7 @@ public int hashCode() { } @Test - public void testMultipleProxiesForIntroductionAdvisor() { + void testMultipleProxiesForIntroductionAdvisor() { TestBean target1 = new TestBean(); target1.setAge(20); TestBean target2 = new TestBean(); @@ -259,7 +257,7 @@ private ITestBean getIntroductionAdvisorProxy(TestBean target) { } @Test - public void testWithNoArgConstructor() { + void testWithNoArgConstructor() { NoArgCtorTestBean target = new NoArgCtorTestBean("b", 1); target.reset(); @@ -274,7 +272,7 @@ public void testWithNoArgConstructor() { } @Test - public void testProxyAProxy() { + void testProxyAProxy() { ITestBean target = new TestBean(); mockTargetSource.setTarget(target); @@ -295,7 +293,7 @@ public void testProxyAProxy() { } @Test - public void testProxyAProxyWithAdditionalInterface() { + void testProxyAProxyWithAdditionalInterface() { ITestBean target = new TestBean(); mockTargetSource.setTarget(target); @@ -353,7 +351,7 @@ public void testProxyAProxyWithAdditionalInterface() { } @Test - public void testExceptionHandling() { + void testExceptionHandling() { ExceptionThrower bean = new ExceptionThrower(); mockTargetSource.setTarget(bean); @@ -376,14 +374,14 @@ public void testExceptionHandling() { } @Test - @SuppressWarnings("resource") - public void testWithDependencyChecking() { - ApplicationContext ctx = new ClassPathXmlApplicationContext(DEPENDENCY_CHECK_CONTEXT, getClass()); - ctx.getBean("testBean"); + void testWithDependencyChecking() { + try (ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(DEPENDENCY_CHECK_CONTEXT, getClass())) { + ctx.getBean("testBean"); + } } @Test - public void testAddAdviceAtRuntime() { + void testAddAdviceAtRuntime() { TestBean bean = new TestBean(); CountingBeforeAdvice cba = new CountingBeforeAdvice(); @@ -405,7 +403,7 @@ public void testAddAdviceAtRuntime() { } @Test - public void testProxyProtectedMethod() { + void testProxyProtectedMethod() { CountingBeforeAdvice advice = new CountingBeforeAdvice(); ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); proxyFactory.addAdvice(advice); @@ -417,15 +415,14 @@ public void testProxyProtectedMethod() { } @Test - public void testProxyTargetClassInCaseOfNoInterfaces() { + void testProxyTargetClassInCaseOfNoInterfaces() { ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); MyBean proxy = (MyBean) proxyFactory.getProxy(); assertThat(proxy.add(1, 3)).isEqualTo(4); } @Test // SPR-13328 - @SuppressWarnings("unchecked") - public void testVarargsWithEnumArray() { + void testVarargsWithEnumArray() { ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); MyBean proxy = (MyBean) proxyFactory.getProxy(); assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); @@ -461,13 +458,13 @@ public interface MyInterface { public enum MyEnum implements MyInterface { - A, B; + A, B } public enum MyOtherEnum implements MyInterface { - C, D; + C, D } @@ -485,7 +482,7 @@ public boolean isFinallyInvoked() { return finallyInvoked; } - public void doTest() throws Exception { + public void doTest() { try { throw new ApplicationContextException("foo"); } @@ -589,7 +586,7 @@ public String toString() { class UnsupportedInterceptor implements MethodInterceptor { @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public Object invoke(MethodInvocation mi) { throw new UnsupportedOperationException(mi.getMethod().getName()); } } 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 2eaecfb6002e..a577598bd5c8 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.aop.framework; -import java.io.Serializable; - import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; @@ -38,8 +36,7 @@ * @author Chris Beams * @since 13.03.2003 */ -@SuppressWarnings("serial") -public class JdkDynamicProxyTests extends AbstractAopProxyTests implements Serializable { +class JdkDynamicProxyTests extends AbstractAopProxyTests { @Override protected Object createProxy(ProxyCreatorSupport as) { @@ -56,13 +53,12 @@ protected AopProxy createAopProxy(AdvisedSupport as) { @Test - public void testNullConfig() { - assertThatIllegalArgumentException().isThrownBy(() -> - new JdkDynamicAopProxy(null)); + void testNullConfig() { + assertThatIllegalArgumentException().isThrownBy(() -> new JdkDynamicAopProxy(null)); } @Test - public void testProxyIsJustInterface() { + void testProxyIsJustInterface() { TestBean raw = new TestBean(); raw.setAge(32); AdvisedSupport pc = new AdvisedSupport(ITestBean.class); @@ -70,14 +66,12 @@ public void testProxyIsJustInterface() { JdkDynamicAopProxy aop = new JdkDynamicAopProxy(pc); Object proxy = aop.getProxy(); - boolean condition = proxy instanceof ITestBean; - assertThat(condition).isTrue(); - boolean condition1 = proxy instanceof TestBean; - assertThat(condition1).isFalse(); + assertThat(proxy instanceof ITestBean).isTrue(); + assertThat(proxy instanceof TestBean).isFalse(); } @Test - public void testInterceptorIsInvokedWithNoTarget() { + void testInterceptorIsInvokedWithNoTarget() { // Test return value final int age = 25; MethodInterceptor mi = (invocation -> age); @@ -91,7 +85,7 @@ public void testInterceptorIsInvokedWithNoTarget() { } @Test - public void testTargetCanGetInvocationWithPrivateClass() { + void testTargetCanGetInvocationWithPrivateClass() { final ExposedInvocationTestBean expectedTarget = new ExposedInvocationTestBean() { @Override protected void assertions(MethodInvocation invocation) { @@ -119,7 +113,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } @Test - public void testProxyNotWrappedIfIncompatible() { + void testProxyNotWrappedIfIncompatible() { FooBar bean = new FooBar(); ProxyCreatorSupport as = new ProxyCreatorSupport(); as.setInterfaces(Foo.class); @@ -131,19 +125,22 @@ public void testProxyNotWrappedIfIncompatible() { } @Test - public void testEqualsAndHashCodeDefined() { - AdvisedSupport as = new AdvisedSupport(Named.class); - as.setTarget(new Person()); - JdkDynamicAopProxy aopProxy = new JdkDynamicAopProxy(as); - Named proxy = (Named) aopProxy.getProxy(); + void testEqualsAndHashCodeDefined() { Named named = new Person(); + AdvisedSupport as = new AdvisedSupport(Named.class); + as.setTarget(named); + + Named proxy = (Named) new JdkDynamicAopProxy(as).getProxy(); + assertThat(proxy).isEqualTo(named); + assertThat(named.hashCode()).isEqualTo(proxy.hashCode()); + + proxy = (Named) new JdkDynamicAopProxy(as).getProxy(); assertThat(proxy).isEqualTo(named); assertThat(named.hashCode()).isEqualTo(proxy.hashCode()); } @Test // SPR-13328 - @SuppressWarnings("unchecked") - public void testVarargsWithEnumArray() { + void testVarargsWithEnumArray() { ProxyFactory proxyFactory = new ProxyFactory(new VarargTestBean()); VarargTestInterface proxy = (VarargTestInterface) proxyFactory.getProxy(); assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); @@ -242,13 +239,13 @@ public interface MyInterface { public enum MyEnum implements MyInterface { - A, B; + A, B } public enum MyOtherEnum implements MyInterface { - C, D; + C, D } } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java index 2a67998c3ae2..d50252e323ff 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-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,10 @@ * * @author Oliver Gierke */ -public class ObjenesisProxyTests { +class ObjenesisProxyTests { @Test - public void appliesAspectToClassWithComplexConstructor() { - @SuppressWarnings("resource") + void appliesAspectToClassWithComplexConstructor() { ApplicationContext context = new ClassPathXmlApplicationContext("ObjenesisProxyTests-context.xml", getClass()); ClassWithComplexConstructor bean = context.getBean(ClassWithComplexConstructor.class); 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 bc051b7fbcb3..481fe6e591ec 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ * @author Chris Beams * @since 13.03.2003 */ -public class ProxyFactoryBeanTests { +class ProxyFactoryBeanTests { private static final Class CLASS = ProxyFactoryBeanTests.class; private static final String CLASSNAME = CLASS.getSimpleName(); @@ -92,7 +92,7 @@ public class ProxyFactoryBeanTests { @BeforeEach - public void setup() throws Exception { + void setup() { DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); parent.registerBeanDefinition("target2", new RootBeanDefinition(TestApplicationListener.class)); this.factory = new DefaultListableBeanFactory(parent); @@ -102,25 +102,25 @@ public void setup() throws Exception { @Test - public void testIsDynamicProxyWhenInterfaceSpecified() { + void testIsDynamicProxyWhenInterfaceSpecified() { ITestBean test1 = (ITestBean) factory.getBean("test1"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test1 is a dynamic proxy").isTrue(); } @Test - public void testIsDynamicProxyWhenInterfaceSpecifiedForPrototype() { + void testIsDynamicProxyWhenInterfaceSpecifiedForPrototype() { ITestBean test1 = (ITestBean) factory.getBean("test2"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test2 is a dynamic proxy").isTrue(); } @Test - public void testIsDynamicProxyWhenAutodetectingInterfaces() { + void testIsDynamicProxyWhenAutodetectingInterfaces() { ITestBean test1 = (ITestBean) factory.getBean("test3"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test3 is a dynamic proxy").isTrue(); } @Test - public void testIsDynamicProxyWhenAutodetectingInterfacesForPrototype() { + void testIsDynamicProxyWhenAutodetectingInterfacesForPrototype() { ITestBean test1 = (ITestBean) factory.getBean("test4"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test4 is a dynamic proxy").isTrue(); } @@ -130,7 +130,7 @@ public void testIsDynamicProxyWhenAutodetectingInterfacesForPrototype() { * interceptor chain and targetSource property. */ @Test - public void testDoubleTargetSourcesAreRejected() { + void testDoubleTargetSourcesAreRejected() { testDoubleTargetSourceIsRejected("doubleTarget"); // Now with conversion from arbitrary bean to a TargetSource testDoubleTargetSourceIsRejected("arbitraryTarget"); @@ -148,7 +148,7 @@ private void testDoubleTargetSourceIsRejected(String name) { } @Test - public void testTargetSourceNotAtEndOfInterceptorNamesIsRejected() { + void testTargetSourceNotAtEndOfInterceptorNamesIsRejected() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(NOTLAST_TARGETSOURCE_CONTEXT, CLASS)); @@ -160,7 +160,7 @@ public void testTargetSourceNotAtEndOfInterceptorNamesIsRejected() { } @Test - public void testGetObjectTypeWithDirectTarget() { + void testGetObjectTypeWithDirectTarget() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); @@ -177,7 +177,7 @@ public void testGetObjectTypeWithDirectTarget() { } @Test - public void testGetObjectTypeWithTargetViaTargetSource() { + void testGetObjectTypeWithTargetViaTargetSource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); ITestBean tb = (ITestBean) bf.getBean("viaTargetSource"); @@ -187,7 +187,7 @@ public void testGetObjectTypeWithTargetViaTargetSource() { } @Test - public void testGetObjectTypeWithNoTargetOrTargetSource() { + void testGetObjectTypeWithNoTargetOrTargetSource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); @@ -198,7 +198,7 @@ public void testGetObjectTypeWithNoTargetOrTargetSource() { } @Test - public void testGetObjectTypeOnUninitializedFactoryBean() { + void testGetObjectTypeOnUninitializedFactoryBean() { ProxyFactoryBean pfb = new ProxyFactoryBean(); assertThat(pfb.getObjectType()).isNull(); } @@ -208,7 +208,7 @@ public void testGetObjectTypeOnUninitializedFactoryBean() { * Interceptors and interfaces and the target are the same. */ @Test - public void testSingletonInstancesAreEqual() { + void testSingletonInstancesAreEqual() { ITestBean test1 = (ITestBean) factory.getBean("test1"); ITestBean test1_1 = (ITestBean) factory.getBean("test1"); //assertTrue("Singleton instances ==", test1 == test1_1); @@ -232,7 +232,7 @@ public void testSingletonInstancesAreEqual() { } @Test - public void testPrototypeInstancesAreNotEqual() { + void testPrototypeInstancesAreNotEqual() { assertThat(factory.getType("prototype")).isAssignableTo(ITestBean.class); ITestBean test2 = (ITestBean) factory.getBean("prototype"); ITestBean test2_1 = (ITestBean) factory.getBean("prototype"); @@ -276,7 +276,7 @@ private Object testPrototypeInstancesAreIndependent(String beanName) { } @Test - public void testCglibPrototypeInstance() { + void testCglibPrototypeInstance() { Object prototype = testPrototypeInstancesAreIndependent("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 +286,7 @@ public void testCglibPrototypeInstance() { * Test invoker is automatically added to manipulate target. */ @Test - public void testAutoInvoker() { + void testAutoInvoker() { String name = "Hieronymous"; TestBean target = (TestBean) factory.getBean("test"); target.setName(name); @@ -295,7 +295,7 @@ public void testAutoInvoker() { } @Test - public void testCanGetFactoryReferenceAndManipulate() { + void testCanGetFactoryReferenceAndManipulate() { ProxyFactoryBean config = (ProxyFactoryBean) factory.getBean("&test1"); assertThat(config.getObjectType()).isAssignableTo(ITestBean.class); assertThat(factory.getType("test1")).isAssignableTo(ITestBean.class); @@ -327,7 +327,7 @@ public void testCanGetFactoryReferenceAndManipulate() { * autowire without ambiguity from target and proxy */ @Test - public void testTargetAsInnerBean() { + void testTargetAsInnerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INNER_BEAN_TARGET_CONTEXT, CLASS)); ITestBean itb = (ITestBean) bf.getBean("testBean"); @@ -343,7 +343,7 @@ public void testTargetAsInnerBean() { * Each instance will be independent. */ @Test - public void testCanAddAndRemoveAspectInterfacesOnPrototype() { + void testCanAddAndRemoveAspectInterfacesOnPrototype() { assertThat(factory.getBean("test2")).as("Shouldn't implement TimeStamped before manipulation") .isNotInstanceOf(TimeStamped.class); @@ -402,7 +402,7 @@ public void testCanAddAndRemoveAspectInterfacesOnPrototype() { * singleton. */ @Test - public void testCanAddAndRemoveAdvicesOnSingleton() { + void testCanAddAndRemoveAdvicesOnSingleton() { ITestBean it = (ITestBean) factory.getBean("test1"); Advised pc = (Advised) it; it.getAge(); @@ -415,7 +415,7 @@ public void testCanAddAndRemoveAdvicesOnSingleton() { } @Test - public void testMethodPointcuts() { + void testMethodPointcuts() { ITestBean tb = (ITestBean) factory.getBean("pointcuts"); PointcutForVoid.reset(); assertThat(PointcutForVoid.methodNames).as("No methods intercepted").isEmpty(); @@ -425,13 +425,12 @@ public void testMethodPointcuts() { tb.getAge(); tb.setName("Tristan"); tb.toString(); - assertThat(PointcutForVoid.methodNames).as("Recorded wrong number of invocations").hasSize(2); - assertThat(PointcutForVoid.methodNames.get(0)).isEqualTo("setAge"); - assertThat(PointcutForVoid.methodNames.get(1)).isEqualTo("setName"); + assertThat(PointcutForVoid.methodNames).as("Recorded wrong number of invocations") + .containsExactly("setAge", "setName"); } @Test - public void testCanAddThrowsAdviceWithoutAdvisor() throws Throwable { + void testCanAddThrowsAdviceWithoutAdvisor() { DefaultListableBeanFactory f = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(f).loadBeanDefinitions(new ClassPathResource(THROWS_ADVICE_CONTEXT, CLASS)); MyThrowsHandler th = (MyThrowsHandler) f.getBean("throwsAdvice"); @@ -464,19 +463,19 @@ public void testCanAddThrowsAdviceWithoutAdvisor() throws Throwable { // TODO put in sep file to check quality of error message /* @Test - public void testNoInterceptorNamesWithoutTarget() { + void testNoInterceptorNamesWithoutTarget() { assertThatExceptionOfType(AopConfigurationException.class).as("Should require interceptor names").isThrownBy(() -> ITestBean tb = (ITestBean) factory.getBean("noInterceptorNamesWithoutTarget")); } @Test - public void testNoInterceptorNamesWithTarget() { + void testNoInterceptorNamesWithTarget() { ITestBean tb = (ITestBean) factory.getBean("noInterceptorNamesWithoutTarget"); } */ @Test - public void testEmptyInterceptorNames() { + void testEmptyInterceptorNames() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INVALID_CONTEXT, CLASS)); assertThat(bf.getBean("emptyInterceptorNames")).isInstanceOf(ITestBean.class); @@ -487,7 +486,7 @@ public void testEmptyInterceptorNames() { * Globals must be followed by a target. */ @Test - public void testGlobalsWithoutTarget() { + void testGlobalsWithoutTarget() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INVALID_CONTEXT, CLASS)); assertThatExceptionOfType(BeanCreationException.class).as("Should require target name").isThrownBy(() -> @@ -502,7 +501,7 @@ public void testGlobalsWithoutTarget() { * to be included in proxiedInterface []. */ @Test - public void testGlobalsCanAddAspectInterfaces() { + void testGlobalsCanAddAspectInterfaces() { AddedGlobalInterface agi = (AddedGlobalInterface) factory.getBean("autoInvoker"); assertThat(agi.globalsAdded()).isEqualTo(-1); @@ -521,7 +520,7 @@ public void testGlobalsCanAddAspectInterfaces() { } @Test - public void testSerializableSingletonProxy() throws Exception { + void testSerializableSingletonProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("serializableSingleton"); @@ -544,7 +543,7 @@ public void testSerializableSingletonProxy() throws Exception { } @Test - public void testSerializablePrototypeProxy() throws Exception { + void testSerializablePrototypeProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("serializablePrototype"); @@ -556,7 +555,7 @@ public void testSerializablePrototypeProxy() throws Exception { } @Test - public void testSerializableSingletonProxyFactoryBean() throws Exception { + void testSerializableSingletonProxyFactoryBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("serializableSingleton"); @@ -569,7 +568,7 @@ public void testSerializableSingletonProxyFactoryBean() throws Exception { } @Test - public void testProxyNotSerializableBecauseOfAdvice() throws Exception { + void testProxyNotSerializableBecauseOfAdvice() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("interceptorNotSerializableSingleton"); @@ -577,7 +576,7 @@ public void testProxyNotSerializableBecauseOfAdvice() throws Exception { } @Test - public void testPrototypeAdvisor() { + void testPrototypeAdvisor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(CONTEXT, CLASS)); @@ -598,7 +597,7 @@ public void testPrototypeAdvisor() { } @Test - public void testPrototypeInterceptorSingletonTarget() { + void testPrototypeInterceptorSingletonTarget() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(CONTEXT, CLASS)); @@ -623,14 +622,14 @@ public void testPrototypeInterceptorSingletonTarget() { * Checks for correct use of getType() by bean factory. */ @Test - public void testInnerBeanTargetUsingAutowiring() { + void testInnerBeanTargetUsingAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(AUTOWIRING_CONTEXT, CLASS)); bf.getBean("testBean"); } @Test - public void testFrozenFactoryBean() { + void testFrozenFactoryBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(FROZEN_CONTEXT, CLASS)); @@ -639,7 +638,7 @@ public void testFrozenFactoryBean() { } @Test - public void testDetectsInterfaces() { + void testDetectsInterfaces() { ProxyFactoryBean fb = new ProxyFactoryBean(); fb.setTarget(new TestBean()); fb.addAdvice(new DebugInterceptor()); @@ -650,7 +649,7 @@ public void testDetectsInterfaces() { } @Test - public void testWithInterceptorNames() { + void testWithInterceptorNames() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("debug", new DebugInterceptor()); @@ -664,7 +663,7 @@ public void testWithInterceptorNames() { } @Test - public void testWithLateInterceptorNames() { + void testWithLateInterceptorNames() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("debug", new DebugInterceptor()); @@ -699,7 +698,7 @@ public PointcutForVoid() { setPointcut(new DynamicMethodMatcherPointcut() { @Override public boolean matches(Method m, @Nullable Class targetClass, Object... args) { - return m.getReturnType() == Void.TYPE; + return m.getReturnType() == void.class; } }); } 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 4fb3d1c930c9..30a45d913bd7 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ private SimpleBeforeAdviceImpl getAdviceImpl(ITestBean tb) { interface SimpleBeforeAdvice extends BeforeAdvice { - void before() throws Throwable; + void before(); } @@ -108,7 +108,7 @@ class SimpleBeforeAdviceImpl implements SimpleBeforeAdvice { private int invocationCounter; @Override - public void before() throws Throwable { + public void before() { ++invocationCounter; } 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 5c3c66ffe8ed..9afebac85f65 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.aop.framework.autoproxy; -import java.io.IOException; - import org.junit.jupiter.api.Test; import org.springframework.aop.Advisor; @@ -48,8 +46,7 @@ * @author Dave Syer * @author Chris Beams */ -@SuppressWarnings("resource") -public class AdvisorAutoProxyCreatorTests { +class AdvisorAutoProxyCreatorTests { private static final Class CLASS = AdvisorAutoProxyCreatorTests.class; private static final String CLASSNAME = CLASS.getSimpleName(); @@ -64,7 +61,7 @@ public class AdvisorAutoProxyCreatorTests { /** * Return a bean factory with attributes and EnterpriseServices configured. */ - protected BeanFactory getBeanFactory() throws IOException { + protected BeanFactory getBeanFactory() { return new ClassPathXmlApplicationContext(DEFAULT_CONTEXT, CLASS); } @@ -75,7 +72,7 @@ protected BeanFactory getBeanFactory() throws IOException { * which are sourced from matching advisors */ @Test - public void testCommonInterceptorAndAdvisor() throws Exception { + void testCommonInterceptorAndAdvisor() { BeanFactory bf = new ClassPathXmlApplicationContext(COMMON_INTERCEPTORS_CONTEXT, CLASS); ITestBean test1 = (ITestBean) bf.getBean("test1"); assertThat(AopUtils.isAopProxy(test1)).isTrue(); @@ -120,7 +117,7 @@ public void testCommonInterceptorAndAdvisor() throws Exception { * hence no proxying, for this bean */ @Test - public void testCustomTargetSourceNoMatch() throws Exception { + void testCustomTargetSourceNoMatch() { BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); ITestBean test = (ITestBean) bf.getBean("test"); assertThat(AopUtils.isAopProxy(test)).isFalse(); @@ -129,7 +126,7 @@ public void testCustomTargetSourceNoMatch() throws Exception { } @Test - public void testCustomPrototypeTargetSource() throws Exception { + void testCustomPrototypeTargetSource() { CountingTestBean.count = 0; BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); ITestBean test = (ITestBean) bf.getBean("prototypeTest"); @@ -145,7 +142,7 @@ public void testCustomPrototypeTargetSource() throws Exception { } @Test - public void testLazyInitTargetSource() throws Exception { + void testLazyInitTargetSource() { CountingTestBean.count = 0; BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); ITestBean test = (ITestBean) bf.getBean("lazyInitTest"); @@ -161,7 +158,7 @@ public void testLazyInitTargetSource() throws Exception { } @Test - public void testQuickTargetSourceCreator() throws Exception { + void testQuickTargetSourceCreator() { ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext(QUICK_TARGETSOURCE_CONTEXT, CLASS); ITestBean test = (ITestBean) bf.getBean("test"); @@ -209,7 +206,7 @@ public void testQuickTargetSourceCreator() throws Exception { } @Test - public void testWithOptimizedProxy() throws Exception { + void testWithOptimizedProxy() { BeanFactory beanFactory = new ClassPathXmlApplicationContext(OPTIMIZED_CONTEXT, CLASS); ITestBean testBean = (ITestBean) beanFactory.getBean("optimizedTestBean"); 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 901c88665d5f..6fc0e394d650 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,11 +55,10 @@ * @author Chris Beams * @since 09.12.2003 */ -@SuppressWarnings("resource") -public class AutoProxyCreatorTests { +class AutoProxyCreatorTests { @Test - public void testBeanNameAutoProxyCreator() { + void testBeanNameAutoProxyCreator() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testInterceptor", TestInterceptor.class); @@ -109,7 +108,7 @@ public void testBeanNameAutoProxyCreator() { } @Test - public void testBeanNameAutoProxyCreatorWithFactoryBeanProxy() { + void testBeanNameAutoProxyCreatorWithFactoryBeanProxy() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testInterceptor", TestInterceptor.class); @@ -143,7 +142,7 @@ public void testBeanNameAutoProxyCreatorWithFactoryBeanProxy() { } @Test - public void testCustomAutoProxyCreator() { + void testCustomAutoProxyCreator() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); sac.registerSingleton("noInterfaces", NoInterfaces.class); @@ -178,7 +177,7 @@ public void testCustomAutoProxyCreator() { } @Test - public void testAutoProxyCreatorWithFallbackToTargetClass() { + void testAutoProxyCreatorWithFallbackToTargetClass() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testAutoProxyCreator", FallbackTestAutoProxyCreator.class); sac.registerSingleton("noInterfaces", NoInterfaces.class); @@ -213,7 +212,7 @@ public void testAutoProxyCreatorWithFallbackToTargetClass() { } @Test - public void testAutoProxyCreatorWithFallbackToDynamicProxy() { + void testAutoProxyCreatorWithFallbackToDynamicProxy() { StaticApplicationContext sac = new StaticApplicationContext(); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -253,7 +252,7 @@ public void testAutoProxyCreatorWithFallbackToDynamicProxy() { } @Test - public void testAutoProxyCreatorWithPackageVisibleMethod() { + void testAutoProxyCreatorWithPackageVisibleMethod() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); sac.registerSingleton("packageVisibleMethodToBeProxied", PackageVisibleMethod.class); @@ -270,7 +269,7 @@ public void testAutoProxyCreatorWithPackageVisibleMethod() { } @Test - public void testAutoProxyCreatorWithFactoryBean() { + void testAutoProxyCreatorWithFactoryBean() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); sac.registerSingleton("singletonFactoryToBeProxied", DummyFactory.class); @@ -290,7 +289,7 @@ public void testAutoProxyCreatorWithFactoryBean() { } @Test - public void testAutoProxyCreatorWithFactoryBeanAndPrototype() { + void testAutoProxyCreatorWithFactoryBeanAndPrototype() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); @@ -314,7 +313,7 @@ public void testAutoProxyCreatorWithFactoryBeanAndPrototype() { } @Test - public void testAutoProxyCreatorWithFactoryBeanAndProxyObjectOnly() { + void testAutoProxyCreatorWithFactoryBeanAndProxyObjectOnly() { StaticApplicationContext sac = new StaticApplicationContext(); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -345,7 +344,7 @@ public void testAutoProxyCreatorWithFactoryBeanAndProxyObjectOnly() { } @Test - public void testAutoProxyCreatorWithFactoryBeanAndProxyFactoryBeanOnly() { + void testAutoProxyCreatorWithFactoryBeanAndProxyFactoryBeanOnly() { StaticApplicationContext sac = new StaticApplicationContext(); MutablePropertyValues pvs = new MutablePropertyValues(); 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 786c22e1e2bc..a378790b27de 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ void ignoreAdvisorThatIsCurrentlyInCreation() { class NullChecker implements MethodBeforeAdvice { @Override - public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + public void before(Method method, Object[] args, @Nullable Object target) { check(args); } 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 e316ac4baecf..b43084809ec9 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -166,7 +166,7 @@ void customTargetSourceCreatorsApplyOnlyToConfiguredBeanNames() { } - private void jdkAssertions(ITestBean tb, int nopInterceptorCount) { + private void jdkAssertions(ITestBean tb, int nopInterceptorCount) { NopInterceptor nop = (NopInterceptor) beanFactory.getBean("nopInterceptor"); assertThat(nop.getCount()).isEqualTo(0); assertThat(AopUtils.isJdkDynamicProxy(tb)).isTrue(); @@ -198,7 +198,7 @@ private void cglibAssertions(TestBean tb) { class CreatesTestBean implements FactoryBean { @Override - public Object getObject() throws Exception { + public Object getObject() { return new TestBean(); } 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 1f215849aa09..91bc65c02bcb 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-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.aop.scope; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -33,13 +34,14 @@ import org.springframework.core.testfixture.io.SerializationTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; /** * @author Rob Harrop * @author Juergen Hoeller * @author Chris Beams */ -public class ScopedProxyTests { +class ScopedProxyTests { private static final Class CLASS = ScopedProxyTests.class; private static final String CLASSNAME = CLASS.getSimpleName(); @@ -51,102 +53,91 @@ public class ScopedProxyTests { @Test // SPR-2108 - public void testProxyAssignable() throws Exception { + void proxyAssignable() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); Object baseMap = bf.getBean("singletonMap"); - boolean condition = baseMap instanceof Map; - assertThat(condition).isTrue(); + assertThat(baseMap).isInstanceOf(Map.class); } @Test - public void testSimpleProxy() throws Exception { + void simpleProxy() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); Object simpleMap = bf.getBean("simpleMap"); - boolean condition1 = simpleMap instanceof Map; - assertThat(condition1).isTrue(); - boolean condition = simpleMap instanceof HashMap; - assertThat(condition).isTrue(); + assertThat(simpleMap).isInstanceOf(HashMap.class); } @Test - public void testScopedOverride() throws Exception { + void scopedOverride() { GenericApplicationContext ctx = new GenericApplicationContext(); new XmlBeanDefinitionReader(ctx).loadBeanDefinitions(OVERRIDE_CONTEXT); SimpleMapScope scope = new SimpleMapScope(); ctx.getBeanFactory().registerScope("request", scope); ctx.refresh(); - ITestBean bean = (ITestBean) ctx.getBean("testBean"); + ITestBean bean = ctx.getBean("testBean", ITestBean.class); assertThat(bean.getName()).isEqualTo("male"); assertThat(bean.getAge()).isEqualTo(99); - assertThat(scope.getMap().containsKey("scopedTarget.testBean")).isTrue(); - assertThat(scope.getMap().get("scopedTarget.testBean").getClass()).isEqualTo(TestBean.class); + assertThat(scope.getMap()).containsKey("scopedTarget.testBean"); + assertThat(scope.getMap().get("scopedTarget.testBean")).isExactlyInstanceOf(TestBean.class); } @Test - public void testJdkScopedProxy() throws Exception { + void jdkScopedProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(TESTBEAN_CONTEXT); bf.setSerializationId("X"); SimpleMapScope scope = new SimpleMapScope(); bf.registerScope("request", scope); - ITestBean bean = (ITestBean) bf.getBean("testBean"); + ITestBean bean = bf.getBean("testBean", ITestBean.class); assertThat(bean).isNotNull(); assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); - boolean condition1 = bean instanceof ScopedObject; - assertThat(condition1).isTrue(); - ScopedObject scoped = (ScopedObject) bean; - assertThat(scoped.getTargetObject().getClass()).isEqualTo(TestBean.class); - bean.setAge(101); + assertThat(bean).asInstanceOf(type(ScopedObject.class)) + .extracting(ScopedObject::getTargetObject) + .isExactlyInstanceOf(TestBean.class); - assertThat(scope.getMap().containsKey("testBeanTarget")).isTrue(); - assertThat(scope.getMap().get("testBeanTarget").getClass()).isEqualTo(TestBean.class); + assertThat(scope.getMap()).containsKey("testBeanTarget"); + assertThat(scope.getMap().get("testBeanTarget")).isExactlyInstanceOf(TestBean.class); + bean.setAge(101); ITestBean deserialized = SerializationTestUtils.serializeAndDeserialize(bean); assertThat(deserialized).isNotNull(); assertThat(AopUtils.isJdkDynamicProxy(deserialized)).isTrue(); - assertThat(bean.getAge()).isEqualTo(101); - boolean condition = deserialized instanceof ScopedObject; - assertThat(condition).isTrue(); - ScopedObject scopedDeserialized = (ScopedObject) deserialized; - assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(TestBean.class); - - bf.setSerializationId(null); + assertThat(deserialized.getAge()).isEqualTo(101); + assertThat(deserialized).asInstanceOf(type(ScopedObject.class)) + .extracting(ScopedObject::getTargetObject) + .isExactlyInstanceOf(TestBean.class); } @Test - public void testCglibScopedProxy() throws Exception { + void cglibScopedProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(LIST_CONTEXT); bf.setSerializationId("Y"); SimpleMapScope scope = new SimpleMapScope(); bf.registerScope("request", scope); - TestBean tb = (TestBean) bf.getBean("testBean"); - assertThat(AopUtils.isCglibProxy(tb.getFriends())).isTrue(); - boolean condition1 = tb.getFriends() instanceof ScopedObject; - assertThat(condition1).isTrue(); - ScopedObject scoped = (ScopedObject) tb.getFriends(); - assertThat(scoped.getTargetObject().getClass()).isEqualTo(ArrayList.class); - tb.getFriends().add("myFriend"); + TestBean tb = bf.getBean("testBean", TestBean.class); + Collection friends = tb.getFriends(); + assertThat(AopUtils.isCglibProxy(friends)).isTrue(); + assertThat(friends).asInstanceOf(type(ScopedObject.class)) + .extracting(ScopedObject::getTargetObject) + .isExactlyInstanceOf(ArrayList.class); - assertThat(scope.getMap().containsKey("scopedTarget.scopedList")).isTrue(); - assertThat(scope.getMap().get("scopedTarget.scopedList").getClass()).isEqualTo(ArrayList.class); + assertThat(scope.getMap()).containsKey("scopedTarget.scopedList"); + assertThat(scope.getMap().get("scopedTarget.scopedList")).isExactlyInstanceOf(ArrayList.class); - ArrayList deserialized = (ArrayList) SerializationTestUtils.serializeAndDeserialize(tb.getFriends()); + friends.add("myFriend"); + ArrayList deserialized = (ArrayList) SerializationTestUtils.serializeAndDeserialize(friends); assertThat(deserialized).isNotNull(); assertThat(AopUtils.isCglibProxy(deserialized)).isTrue(); - assertThat(deserialized.contains("myFriend")).isTrue(); - boolean condition = deserialized instanceof ScopedObject; - assertThat(condition).isTrue(); - ScopedObject scopedDeserialized = (ScopedObject) deserialized; - assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(ArrayList.class); - - bf.setSerializationId(null); + assertThat(deserialized).contains("myFriend"); + assertThat(deserialized).asInstanceOf(type(ScopedObject.class)) + .extracting(ScopedObject::getTargetObject) + .isExactlyInstanceOf(ArrayList.class); } } 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 e4ecb2bb655e..18dbf63832d4 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,7 @@ class CommonsPool2TargetSourceTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - void setUp() throws Exception { + void setUp() { this.beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( new ClassPathResource(getClass().getSimpleName() + "-context.xml", getClass())); @@ -186,8 +186,8 @@ void testHitMaxSizeLoadedFromContext() throws Exception { pooledInstances[9] = targetSource.getTarget(); // release all objects - for (int i = 0; i < pooledInstances.length; i++) { - targetSource.releaseTarget(pooledInstances[i]); + for (Object pooledInstance : pooledInstances) { + targetSource.releaseTarget(pooledInstance); } } 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 afd890a78b5c..dd54e086cc36 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ void SPR8434() { } - static abstract class GenericServiceImpl { + abstract static class GenericServiceImpl { public abstract void setObject(D object); } 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 f2f3e2af9e18..48d19fbf7e81 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,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,12 +40,18 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Integration tests for handling JSR-303 {@link jakarta.inject.Qualifier} annotations. + * Integration tests for handling JSR-330 {@link jakarta.inject.Qualifier} and + * {@link javax.inject.Qualifier} annotations. * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ -public class InjectAnnotationAutowireContextTests { +class InjectAnnotationAutowireContextTests { + + private static final String PERSON1 = "person1"; + + private static final String PERSON2 = "person2"; private static final String JUERGEN = "juergen"; @@ -52,64 +59,61 @@ public class InjectAnnotationAutowireContextTests { @Test - public 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 - public 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 - public 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 - public 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 +122,14 @@ public void testAutowiredFieldWithSingleQualifiedCandidate() { } @Test - public 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 +138,14 @@ public void testAutowiredMethodParameterWithSingleQualifiedCandidate() { } @Test - public 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 +154,17 @@ public void testAutowiredMethodParameterWithStaticallyQualifiedCandidate() { } @Test - public 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 +173,14 @@ public void testAutowiredMethodParameterWithStaticallyQualifiedCandidateAmongOth } @Test - public 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 +189,7 @@ public void testAutowiredConstructorArgumentWithSingleQualifiedCandidate() { } @Test - public void testAutowiredFieldWithMultipleNonQualifiedCandidates() { + void autowiredFieldWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -197,21 +197,20 @@ public 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 - public void testAutowiredMethodParameterWithMultipleNonQualifiedCandidates() { + void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -219,21 +218,20 @@ public 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 - public void testAutowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { + void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -241,18 +239,17 @@ public 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 - public void testAutowiredFieldResolvesQualifiedCandidate() { + void autowiredFieldResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -261,10 +258,9 @@ public 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 +268,7 @@ public void testAutowiredFieldResolvesQualifiedCandidate() { } @Test - public void testAutowiredMethodParameterResolvesQualifiedCandidate() { + void autowiredMethodParameterResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -281,10 +277,9 @@ public 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 +288,7 @@ public void testAutowiredMethodParameterResolvesQualifiedCandidate() { } @Test - public void testAutowiredConstructorArgumentResolvesQualifiedCandidate() { + void autowiredConstructorArgumentResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -302,10 +297,9 @@ public 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 +307,28 @@ public 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 // gh-33345 + void autowiredConstructorArgumentResolvesJavaxNamedCandidate() { + Class testBeanClass = JavaxNamedConstructorArgumentTestBean.class; + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(testBeanClass, JavaxCat.class, JavaxDog.class); + JavaxNamedConstructorArgumentTestBean bean = context.getBean(testBeanClass); + assertThat(bean.getAnimal1().getName()).isEqualTo("Javax Tiger"); + assertThat(bean.getAnimal2().getName()).isEqualTo("Javax Fido"); + } + @Test - public void testAutowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { + void autowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -324,10 +338,9 @@ public void testAutowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoVal 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 +349,7 @@ public void testAutowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoVal } @Test - public void testAutowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { + void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -346,21 +359,20 @@ public void testAutowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflict 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 - public void testAutowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { + void autowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -370,10 +382,9 @@ public void testAutowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnB 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 +393,7 @@ public void testAutowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnB } @Test - public void testAutowiredFieldResolvesWithMultipleQualifierValues() { + void autowiredFieldResolvesWithMultipleQualifierValues() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -396,10 +407,9 @@ public 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 +418,7 @@ public void testAutowiredFieldResolvesWithMultipleQualifierValues() { } @Test - public void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { + void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -423,21 +433,20 @@ public void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConfli 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 - public void testAutowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { + void autowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -452,10 +461,9 @@ public void testAutowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefa 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 +472,7 @@ public void testAutowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefa } @Test - public void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { + void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -479,38 +487,37 @@ public void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultip 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 - public 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 +550,7 @@ public Person getPerson() { private static class QualifiedConstructorArgumentTestBean { - private Person person; + private final Person person; @Inject public QualifiedConstructorArgumentTestBean(@TestQualifier Person person) { @@ -557,6 +564,52 @@ 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; + } + } + + + static class JavaxNamedConstructorArgumentTestBean { + + private final Animal animal1; + private final Animal animal2; + + @javax.inject.Inject + public JavaxNamedConstructorArgumentTestBean(@javax.inject.Named("Cat") Animal animal1, + @javax.inject.Named("Dog") Animal animal2) { + + this.animal1 = animal1; + this.animal2 = animal2; + } + + public Animal getAnimal1() { + return this.animal1; + } + + public Animal getAnimal2() { + return this.animal2; + } + } + + public static class QualifiedFieldWithDefaultValueTestBean { @Inject @@ -593,13 +646,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 +689,52 @@ public QualifiedPerson(String name) { } + interface Animal { + + String getName(); + } + + + @jakarta.inject.Named("Cat") + static class JakartaCat implements Animal { + + @Override + public String getName() { + return "Jakarta Tiger"; + } + } + + + @javax.inject.Named("Cat") + static class JavaxCat implements Animal { + + @Override + public String getName() { + return "Javax Tiger"; + } + } + + + @jakarta.inject.Named("Dog") + static class JakartaDog implements Animal { + + @Override + public String getName() { + return "Jakarta Fido"; + } + } + + + @javax.inject.Named("Dog") + static class JavaxDog implements Animal { + + @Override + public String getName() { + return "Javax Fido"; + } + } + + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java index f126d20473f4..01117baede55 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ * @author Chris Beams * @author Sam Brannen */ -public class QualifierAnnotationAutowireContextTests { +class QualifierAnnotationAutowireContextTests { private static final String JUERGEN = "juergen"; @@ -53,7 +53,7 @@ public class QualifierAnnotationAutowireContextTests { @Test - public void autowiredFieldWithSingleNonQualifiedCandidate() { + void autowiredFieldWithSingleNonQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -63,16 +63,16 @@ public void autowiredFieldWithSingleNonQualifiedCandidate() { 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 - public void autowiredMethodParameterWithSingleNonQualifiedCandidate() { + void autowiredMethodParameterWithSingleNonQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -81,17 +81,18 @@ public void autowiredMethodParameterWithSingleNonQualifiedCandidate() { 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 - public void autowiredConstructorArgumentWithSingleNonQualifiedCandidate() { + void autowiredConstructorArgumentWithSingleNonQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -100,13 +101,14 @@ public void autowiredConstructorArgumentWithSingleNonQualifiedCandidate() { 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 - public void autowiredFieldWithSingleQualifiedCandidate() { + void autowiredFieldWithSingleQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -121,7 +123,7 @@ public void autowiredFieldWithSingleQualifiedCandidate() { } @Test - public void autowiredMethodParameterWithSingleQualifiedCandidate() { + void autowiredMethodParameterWithSingleQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -138,7 +140,7 @@ public void autowiredMethodParameterWithSingleQualifiedCandidate() { } @Test - public void autowiredMethodParameterWithStaticallyQualifiedCandidate() { + void autowiredMethodParameterWithStaticallyQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -155,7 +157,7 @@ public void autowiredMethodParameterWithStaticallyQualifiedCandidate() { } @Test - public void autowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers() { + void autowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -175,7 +177,7 @@ public void autowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers( } @Test - public void autowiredConstructorArgumentWithSingleQualifiedCandidate() { + void autowiredConstructorArgumentWithSingleQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); @@ -192,7 +194,7 @@ public void autowiredConstructorArgumentWithSingleQualifiedCandidate() { } @Test - public void autowiredFieldWithMultipleNonQualifiedCandidates() { + void autowiredFieldWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -205,16 +207,17 @@ public void autowiredFieldWithMultipleNonQualifiedCandidates() { 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 - public void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { + void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -227,16 +230,17 @@ public void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { 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 - public void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { + void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -249,13 +253,14 @@ public void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { 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 - public void autowiredFieldResolvesQualifiedCandidate() { + void autowiredFieldResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -275,7 +280,7 @@ public void autowiredFieldResolvesQualifiedCandidate() { } @Test - public void autowiredFieldResolvesMetaQualifiedCandidate() { + void autowiredFieldResolvesMetaQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -295,7 +300,7 @@ public void autowiredFieldResolvesMetaQualifiedCandidate() { } @Test - public void autowiredMethodParameterResolvesQualifiedCandidate() { + void autowiredMethodParameterResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -316,7 +321,7 @@ public void autowiredMethodParameterResolvesQualifiedCandidate() { } @Test - public void autowiredConstructorArgumentResolvesQualifiedCandidate() { + void autowiredConstructorArgumentResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -337,7 +342,7 @@ public void autowiredConstructorArgumentResolvesQualifiedCandidate() { } @Test - public void autowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { + void autowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -359,7 +364,7 @@ public void autowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOn } @Test - public void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { + void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -374,16 +379,17 @@ public void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingV 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 - public void autowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { + void autowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -405,7 +411,7 @@ public void autowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanD } @Test - public void autowiredFieldResolvesWithMultipleQualifierValues() { + void autowiredFieldResolvesWithMultipleQualifierValues() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -431,7 +437,7 @@ public void autowiredFieldResolvesWithMultipleQualifierValues() { } @Test - public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { + void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -451,16 +457,17 @@ public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictin 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 - public void autowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { + void autowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -487,7 +494,7 @@ public void autowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultV } @Test - public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { + void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -507,16 +514,17 @@ public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMa 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 - public void autowiredFieldResolvesWithBaseQualifierAndDefaultValue() { + void autowiredFieldResolvesWithBaseQualifierAndDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -537,7 +545,7 @@ public void autowiredFieldResolvesWithBaseQualifierAndDefaultValue() { } @Test - public void autowiredFieldResolvesWithBaseQualifierAndNonDefaultValue() { + void autowiredFieldResolvesWithBaseQualifierAndNonDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue("the real juergen"); @@ -559,7 +567,7 @@ public void autowiredFieldResolvesWithBaseQualifierAndNonDefaultValue() { } @Test - public void autowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMultipleMatchingCandidates() { + void autowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMultipleMatchingCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue("the real juergen"); @@ -574,9 +582,10 @@ public void autowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMu 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")); } @@ -752,7 +761,7 @@ public DefaultValueQualifiedPerson(String name) { @Qualifier @interface TestQualifierWithMultipleAttributes { - String value() default "default"; + String[] value() default "default"; int number(); } 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 e65b730e6938..153f0f515a0e 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class LookupMethodWrappedByCglibProxyTests { +class LookupMethodWrappedByCglibProxyTests { private static final Class CLASS = LookupMethodWrappedByCglibProxyTests.class; private static final String CLASSNAME = CLASS.getSimpleName(); @@ -43,13 +43,13 @@ public class LookupMethodWrappedByCglibProxyTests { private ApplicationContext applicationContext; @BeforeEach - public void setUp() { + void setUp() { this.applicationContext = new ClassPathXmlApplicationContext(CONTEXT, CLASS); resetInterceptor(); } @Test - public void testAutoProxiedLookup() { + void testAutoProxiedLookup() { OverloadLookup olup = (OverloadLookup) applicationContext.getBean("autoProxiedOverload"); ITestBean jenny = olup.newTestBean(); assertThat(jenny.getName()).isEqualTo("Jenny"); @@ -58,7 +58,7 @@ public void testAutoProxiedLookup() { } @Test - public void testRegularlyProxiedLookup() { + void testRegularlyProxiedLookup() { 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 49236e0422a7..575a7fed5c64 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class QualifierAnnotationTests { +class QualifierAnnotationTests { private static final String CLASSNAME = QualifierAnnotationTests.class.getName(); @@ -53,18 +53,19 @@ public class QualifierAnnotationTests { @Test - public void testNonQualifiedFieldFails() { + void testNonQualifiedFieldFails() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); context.registerSingleton("testBean", NonQualifiedTestBean.class); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .withMessageContaining("found 6"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .withMessageContaining("found 6"); } @Test - public void testQualifiedByValue() { + void testQualifiedByValue() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -76,7 +77,7 @@ public void testQualifiedByValue() { } @Test - public void testQualifiedByParentValue() { + void testQualifiedByParentValue() { StaticApplicationContext parent = new StaticApplicationContext(); GenericBeanDefinition parentLarry = new GenericBeanDefinition(); parentLarry.setBeanClass(Person.class); @@ -101,7 +102,7 @@ public void testQualifiedByParentValue() { } @Test - public void testQualifiedByBeanName() { + void testQualifiedByBeanName() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -110,11 +111,12 @@ public void testQualifiedByBeanName() { QualifiedByBeanNameTestBean testBean = (QualifiedByBeanNameTestBean) context.getBean("testBean"); Person person = testBean.getLarry(); assertThat(person.getName()).isEqualTo("LarryBean"); - assertThat(testBean.myProps != null && testBean.myProps.isEmpty()).isTrue(); + assertThat(testBean.myProps).isNotNull(); + assertThat(testBean.myProps).isEmpty(); } @Test - public void testQualifiedByFieldName() { + void testQualifiedByFieldName() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -126,7 +128,7 @@ public void testQualifiedByFieldName() { } @Test - public void testQualifiedByParameterName() { + void testQualifiedByParameterName() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -138,7 +140,7 @@ public void testQualifiedByParameterName() { } @Test - public void testQualifiedByAlias() { + void testQualifiedByAlias() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -150,7 +152,7 @@ public void testQualifiedByAlias() { } @Test - public void testQualifiedByAnnotation() { + void testQualifiedByAnnotation() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -162,7 +164,7 @@ public void testQualifiedByAnnotation() { } @Test - public void testQualifiedByCustomValue() { + void testQualifiedByCustomValue() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -174,7 +176,7 @@ public void testQualifiedByCustomValue() { } @Test - public void testQualifiedByAnnotationValue() { + void testQualifiedByAnnotationValue() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -186,18 +188,19 @@ public void testQualifiedByAnnotationValue() { } @Test - public void testQualifiedByAttributesFailsWithoutCustomQualifierRegistered() { + void testQualifiedByAttributesFailsWithoutCustomQualifierRegistered() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); context.registerSingleton("testBean", QualifiedByAttributesTestBean.class); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .withMessageContaining("found 6"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .withMessageContaining("found 6"); } @Test - public void testQualifiedByAttributesWithCustomQualifierRegistered() { + void testQualifiedByAttributesWithCustomQualifierRegistered() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -214,7 +217,7 @@ public void testQualifiedByAttributesWithCustomQualifierRegistered() { } @Test - public void testInterfaceWithOneQualifiedFactoryAndOneQualifiedBean() { + void testInterfaceWithOneQualifiedFactoryAndOneQualifiedBean() { 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/XmlBeanFactoryTestTypes.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java index 01c0f5413df3..06345c821fc4 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -313,7 +313,7 @@ class FixedMethodReplacer implements MethodReplacer { public static final String VALUE = "fixedMethodReplacer"; @Override - public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { + public Object reimplement(Object obj, Method method, Object[] args) { return VALUE; } } @@ -418,7 +418,7 @@ public String replaceMe(int someParam) { @Override public String replaceMe(String someParam) { - return "replaceMe:" + someParam; + return "replaceMe:" + someParam; } } @@ -587,7 +587,7 @@ public Object postProcessAfterInitialization(Object bean, String name) throws Be class ReverseMethodReplacer implements MethodReplacer, Serializable { @Override - public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { + public Object reimplement(Object obj, Method method, Object[] args) { String s = (String) args[0]; return new StringBuilder(s).reverse().toString(); } 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 d42775bd2ad8..689a09490948 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,13 @@ import java.io.InputStreamReader; import java.io.StringWriter; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; @@ -56,6 +61,8 @@ import org.springframework.beans.testfixture.beans.IndexedTestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.UrlResource; @@ -725,7 +732,7 @@ void initializingBeanAndInitMethod() { InitAndIB iib = (InitAndIB) xbf.getBean("init-and-ib"); assertThat(InitAndIB.constructed).isTrue(); assertThat(iib.afterPropertiesSetInvoked && iib.initMethodInvoked).isTrue(); - assertThat(!iib.destroyed && !iib.customDestroyed).isTrue(); + assertThat(iib.destroyed && !iib.customDestroyed).isFalse(); xbf.destroySingletons(); assertThat(iib.destroyed && iib.customDestroyed).isTrue(); xbf.destroySingletons(); @@ -746,7 +753,7 @@ void initializingBeanAndSameInitMethod() { InitAndIB iib = (InitAndIB) xbf.getBean("ib-same-init"); assertThat(InitAndIB.constructed).isTrue(); assertThat(iib.afterPropertiesSetInvoked && !iib.initMethodInvoked).isTrue(); - assertThat(!iib.destroyed && !iib.customDestroyed).isTrue(); + assertThat(iib.destroyed && !iib.customDestroyed).isFalse(); xbf.destroySingletons(); assertThat(iib.destroyed && !iib.customDestroyed).isTrue(); xbf.destroySingletons(); @@ -842,7 +849,7 @@ private void doTestAutowire(DefaultListableBeanFactory xbf) { DependenciesBean rod5 = (DependenciesBean) xbf.getBean("rod5"); // Should not have been autowired - assertThat((Object) rod5.getSpouse()).isNull(); + assertThat(rod5.getSpouse()).isNull(); BeanFactory appCtx = (BeanFactory) xbf.getBean("childAppCtx"); assertThat(appCtx.containsBean("rod1")).isTrue(); @@ -944,7 +951,7 @@ void relatedCausesFromConstructorResolution() { catch (BeanCreationException ex) { ex.printStackTrace(); assertThat(ex.toString()).contains("touchy"); - assertThat((Object) ex.getRelatedCauses()).isNull(); + assertThat(ex.getRelatedCauses()).isNull(); } } @@ -1336,6 +1343,15 @@ void replaceMethodOverrideWithSetterInjection() { assertThat(dos.lastArg).isEqualTo(s2); } + @Test // gh-31826 + void replaceNonOverloadedInterfaceMethodWithoutSpecifyingExplicitArgTypes() { + try (ConfigurableApplicationContext context = + new ClassPathXmlApplicationContext(DELEGATION_OVERRIDES_CONTEXT.getPath())) { + EchoService echoService = context.getBean(EchoService.class); + assertThat(echoService.echo("foo", "bar")).containsExactly("bar", "foo"); + } + } + @Test void lookupOverrideOneMethodWithConstructorInjection() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); @@ -1595,7 +1611,7 @@ static class DoSomethingReplacer implements MethodReplacer { public Object lastArg; @Override - public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { + public Object reimplement(Object obj, Method method, Object[] args) { assertThat(args).hasSize(1); assertThat(method.getName()).isEqualTo("doSomething"); lastArg = args[0]; @@ -1652,7 +1668,7 @@ public void afterPropertiesSet() { } /** Init method */ - public void customInit() throws IOException { + public void customInit() { assertThat(this.afterPropertiesSetInvoked).isTrue(); if (this.initMethodInvoked) { throw new IllegalStateException("Already customInitialized"); @@ -1891,3 +1907,20 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw } } + +interface EchoService { + + String[] echo(Object... objects); +} + +class ReverseArrayMethodReplacer implements MethodReplacer { + + @Override + public Object reimplement(Object obj, Method method, Object[] args) { + List list = Arrays.stream((Object[]) args[0]) + .map(Object::toString) + .collect(Collectors.toCollection(ArrayList::new)); + Collections.reverse(list); + return list.toArray(String[]::new); + } +} 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 f6336a5f51b4..08a6eddbd627 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,14 +64,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Unit tests for custom XML namespace handler implementations. + * Tests for custom XML namespace handler implementations. * * @author Rob Harrop * @author Rick Evans * @author Chris Beams * @author Juergen Hoeller */ -public class CustomNamespaceHandlerTests { +class CustomNamespaceHandlerTests { private static final Class CLASS = CustomNamespaceHandlerTests.class; private static final String CLASSNAME = CLASS.getSimpleName(); @@ -85,7 +85,7 @@ public class CustomNamespaceHandlerTests { @BeforeEach - public void setUp() throws Exception { + void setUp() { NamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(CLASS.getClassLoader(), NS_PROPS); this.beanFactory = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); @@ -98,19 +98,19 @@ public void setUp() throws Exception { @Test - public void testSimpleParser() throws Exception { + void testSimpleParser() { TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); assertTestBean(bean); } @Test - public void testSimpleDecorator() throws Exception { + void testSimpleDecorator() { TestBean bean = (TestBean) this.beanFactory.getBean("customisedTestBean"); assertTestBean(bean); } @Test - public void testProxyingDecorator() throws Exception { + void testProxyingDecorator() { ITestBean bean = (ITestBean) this.beanFactory.getBean("debuggingTestBean"); assertTestBean(bean); assertThat(AopUtils.isAopProxy(bean)).isTrue(); @@ -120,9 +120,9 @@ public void testProxyingDecorator() throws Exception { } @Test - public void testProxyingDecoratorNoInstance() throws Exception { + void testProxyingDecoratorNoInstance() { String[] beanNames = this.beanFactory.getBeanNamesForType(ApplicationListener.class); - assertThat(Arrays.asList(beanNames).contains("debuggingTestBeanNoInstance")).isTrue(); + assertThat(Arrays.asList(beanNames)).contains("debuggingTestBeanNoInstance"); assertThat(this.beanFactory.getType("debuggingTestBeanNoInstance")).isEqualTo(ApplicationListener.class); assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> this.beanFactory.getBean("debuggingTestBeanNoInstance")) @@ -131,7 +131,7 @@ public void testProxyingDecoratorNoInstance() throws Exception { } @Test - public void testChainedDecorators() throws Exception { + void testChainedDecorators() { ITestBean bean = (ITestBean) this.beanFactory.getBean("chainedTestBean"); assertTestBean(bean); assertThat(AopUtils.isAopProxy(bean)).isTrue(); @@ -142,27 +142,27 @@ public void testChainedDecorators() throws Exception { } @Test - public void testDecorationViaAttribute() throws Exception { + void testDecorationViaAttribute() { BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("decorateWithAttribute"); assertThat(beanDefinition.getAttribute("objectName")).isEqualTo("foo"); } @Test // SPR-2728 - public void testCustomElementNestedWithinUtilList() throws Exception { + public void testCustomElementNestedWithinUtilList() { List things = (List) this.beanFactory.getBean("list.of.things"); assertThat(things).isNotNull(); assertThat(things).hasSize(2); } @Test // SPR-2728 - public void testCustomElementNestedWithinUtilSet() throws Exception { + public void testCustomElementNestedWithinUtilSet() { Set things = (Set) this.beanFactory.getBean("set.of.things"); assertThat(things).isNotNull(); assertThat(things).hasSize(2); } @Test // SPR-2728 - public void testCustomElementNestedWithinUtilMap() throws Exception { + public void testCustomElementNestedWithinUtilMap() { Map things = (Map) this.beanFactory.getBean("map.of.things"); assertThat(things).isNotNull(); assertThat(things).hasSize(2); @@ -260,7 +260,7 @@ public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, Element element = (Element) node; BeanDefinition def = definition.getBeanDefinition(); - MutablePropertyValues mpvs = (def.getPropertyValues() == null) ? new MutablePropertyValues() : def.getPropertyValues(); + MutablePropertyValues mpvs = (def.getPropertyValues() == null ? new MutablePropertyValues() : def.getPropertyValues()); mpvs.add("name", element.getAttribute("name")); mpvs.add("age", element.getAttribute("age")); 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 517ba8d6f7d8..9d358b81cdc2 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-2024 the original author 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,15 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; @@ -166,6 +170,196 @@ void spr14853AdaptsToOptionalWithSync() { TestBean tb2 = bean.findById("tb1").get(); assertThat(tb2).isNotSameAs(tb); assertThat(cache.get("tb1").get()).isSameAs(tb2); + + context.close(); + } + + @Test + void spr14235AdaptsToCompletableFuture() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FutureService.class); + Spr14235FutureService bean = context.getBean(Spr14235FutureService.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = bean.findById("tb1").join(); + assertThat(tb).isNotNull(); + assertThat(bean.findById("tb1").join()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + bean.clear().join(); + TestBean tb2 = bean.findById("tb1").join(); + assertThat(tb2).isNotNull(); + assertThat(tb2).isNotSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb2); + + bean.clear().join(); + bean.insertItem(tb).join(); + assertThat(bean.findById("tb1").join()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + tb = bean.findById("tb2").join(); + assertThat(tb).isNotNull(); + assertThat(bean.findById("tb2").join()).isNotSameAs(tb); + assertThat(cache.get("tb2")).isNull(); + + assertThat(bean.findByIdEmpty("").join()).isNull(); + assertThat(cache.get("").get()).isNull(); + assertThat(bean.findByIdEmpty("").join()).isNull(); + + context.close(); + } + + @Test + void spr14235AdaptsToCompletableFutureWithSync() throws Exception { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FutureServiceSync.class); + Spr14235FutureServiceSync bean = context.getBean(Spr14235FutureServiceSync.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = bean.findById("tb1").get(); + assertThat(bean.findById("tb1").get()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + cache.clear(); + TestBean tb2 = bean.findById("tb1").get(); + assertThat(tb2).isNotSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb2); + + cache.clear(); + bean.insertItem(tb); + assertThat(bean.findById("tb1").get()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + assertThat(bean.findById("").join()).isNull(); + assertThat(cache.get("").get()).isNull(); + assertThat(bean.findById("").join()).isNull(); + + context.close(); + } + + @Test + void spr14235AdaptsToReactorMono() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235MonoService.class); + Spr14235MonoService bean = context.getBean(Spr14235MonoService.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = bean.findById("tb1").block(); + assertThat(tb).isNotNull(); + assertThat(bean.findById("tb1").block()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + bean.clear().block(); + TestBean tb2 = bean.findById("tb1").block(); + assertThat(tb2).isNotNull(); + assertThat(tb2).isNotSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb2); + + bean.clear().block(); + bean.insertItem(tb).block(); + assertThat(bean.findById("tb1").block()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + tb = bean.findById("tb2").block(); + assertThat(tb).isNotNull(); + assertThat(bean.findById("tb2").block()).isNotSameAs(tb); + assertThat(cache.get("tb2")).isNull(); + + assertThat(bean.findByIdEmpty("").block()).isNull(); + assertThat(cache.get("").get()).isNull(); + assertThat(bean.findByIdEmpty("").block()).isNull(); + + context.close(); + } + + @Test + void spr14235AdaptsToReactorMonoWithSync() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235MonoServiceSync.class); + Spr14235MonoServiceSync bean = context.getBean(Spr14235MonoServiceSync.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = bean.findById("tb1").block(); + assertThat(bean.findById("tb1").block()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + cache.clear(); + TestBean tb2 = bean.findById("tb1").block(); + assertThat(tb2).isNotSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb2); + + cache.clear(); + bean.insertItem(tb); + assertThat(bean.findById("tb1").block()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + assertThat(bean.findById("").block()).isNull(); + assertThat(cache.get("").get()).isNull(); + assertThat(bean.findById("").block()).isNull(); + + context.close(); + } + + @Test + void spr14235AdaptsToReactorFlux() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FluxService.class); + Spr14235FluxService bean = context.getBean(Spr14235FluxService.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + List tb = bean.findById("tb1").collectList().block(); + assertThat(tb).isNotEmpty(); + assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb); + assertThat(cache.get("tb1").get()).isEqualTo(tb); + + bean.clear().blockLast(); + List tb2 = bean.findById("tb1").collectList().block(); + assertThat(tb2).isNotEmpty(); + assertThat(tb2).isNotEqualTo(tb); + assertThat(cache.get("tb1").get()).isEqualTo(tb2); + + bean.clear().blockLast(); + bean.insertItem("tb1", tb).blockLast(); + assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb); + assertThat(cache.get("tb1").get()).isEqualTo(tb); + + tb = bean.findById("tb2").collectList().block(); + assertThat(tb).isNotEmpty(); + assertThat(bean.findById("tb2").collectList().block()).isNotEqualTo(tb); + assertThat(cache.get("tb2")).isNull(); + + assertThat(bean.findByIdEmpty("").collectList().block()).isEmpty(); + assertThat(cache.get("").get()).isEqualTo(Collections.emptyList()); + assertThat(bean.findByIdEmpty("").collectList().block()).isEmpty(); + + context.close(); + } + + @Test + void spr14235AdaptsToReactorFluxWithSync() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FluxServiceSync.class); + Spr14235FluxServiceSync bean = context.getBean(Spr14235FluxServiceSync.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + List tb = bean.findById("tb1").collectList().block(); + assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb); + assertThat(cache.get("tb1").get()).isEqualTo(tb); + + cache.clear(); + List tb2 = bean.findById("tb1").collectList().block(); + assertThat(tb2).isNotEqualTo(tb); + assertThat(cache.get("tb1").get()).isEqualTo(tb2); + + cache.clear(); + bean.insertItem("tb1", tb); + assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb); + assertThat(cache.get("tb1").get()).isEqualTo(tb); + + assertThat(bean.findById("").collectList().block()).isEmpty(); + assertThat(cache.get("").get()).isEqualTo(Collections.emptyList()); + assertThat(bean.findById("").collectList().block()).isEmpty(); + context.close(); } @@ -391,6 +585,168 @@ public Spr14230Service service() { } + public static class Spr14235FutureService { + + private boolean emptyCalled; + + @Cacheable(value = "itemCache", unless = "#result.name == 'tb2'") + public CompletableFuture findById(String id) { + return CompletableFuture.completedFuture(new TestBean(id)); + } + + @Cacheable(value = "itemCache") + public CompletableFuture findByIdEmpty(String id) { + assertThat(emptyCalled).isFalse(); + emptyCalled = true; + return CompletableFuture.completedFuture(null); + } + + @CachePut(cacheNames = "itemCache", key = "#item.name") + public CompletableFuture insertItem(TestBean item) { + return CompletableFuture.completedFuture(item); + } + + @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0") + public CompletableFuture clear() { + return CompletableFuture.completedFuture(1); + } + } + + + public static class Spr14235FutureServiceSync { + + private boolean emptyCalled; + + @Cacheable(value = "itemCache", sync = true) + public CompletableFuture findById(String id) { + if (id.isEmpty()) { + assertThat(emptyCalled).isFalse(); + emptyCalled = true; + return CompletableFuture.completedFuture(null); + } + return CompletableFuture.completedFuture(new TestBean(id)); + } + + @CachePut(cacheNames = "itemCache", key = "#item.name") + public TestBean insertItem(TestBean item) { + return item; + } + } + + + public static class Spr14235MonoService { + + private boolean emptyCalled; + + @Cacheable(value = "itemCache", unless = "#result.name == 'tb2'") + public Mono findById(String id) { + return Mono.just(new TestBean(id)); + } + + @Cacheable(value = "itemCache") + public Mono findByIdEmpty(String id) { + assertThat(emptyCalled).isFalse(); + emptyCalled = true; + return Mono.empty(); + } + + @CachePut(cacheNames = "itemCache", key = "#item.name") + public Mono insertItem(TestBean item) { + return Mono.just(item); + } + + @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0") + public Mono clear() { + return Mono.just(1); + } + } + + + public static class Spr14235MonoServiceSync { + + private boolean emptyCalled; + + @Cacheable(value = "itemCache", sync = true) + public Mono findById(String id) { + if (id.isEmpty()) { + assertThat(emptyCalled).isFalse(); + emptyCalled = true; + return Mono.empty(); + } + return Mono.just(new TestBean(id)); + } + + @CachePut(cacheNames = "itemCache", key = "#item.name") + public TestBean insertItem(TestBean item) { + return item; + } + } + + + public static class Spr14235FluxService { + + private int counter = 0; + + private boolean emptyCalled; + + @Cacheable(value = "itemCache", unless = "#result[0].name == 'tb2'") + public Flux findById(String id) { + return Flux.just(new TestBean(id), new TestBean(id + (counter++))); + } + + @Cacheable(value = "itemCache") + public Flux findByIdEmpty(String id) { + assertThat(emptyCalled).isFalse(); + emptyCalled = true; + return Flux.empty(); + } + + @CachePut(cacheNames = "itemCache", key = "#id") + public Flux insertItem(String id, List item) { + return Flux.fromIterable(item); + } + + @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0") + public Flux clear() { + return Flux.just(1); + } + } + + + public static class Spr14235FluxServiceSync { + + private int counter = 0; + + private boolean emptyCalled; + + @Cacheable(value = "itemCache", sync = true) + public Flux findById(String id) { + if (id.isEmpty()) { + assertThat(emptyCalled).isFalse(); + emptyCalled = true; + return Flux.empty(); + } + return Flux.just(new TestBean(id), new TestBean(id + (counter++))); + } + + @CachePut(cacheNames = "itemCache", key = "#id") + public List insertItem(String id, List item) { + return item; + } + } + + + @Configuration + @EnableCaching + public static class Spr14235Config { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + } + + public static class Spr14853Service { @Cacheable(value = "itemCache", sync = true) 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 01867a4148c3..b511b2de05ac 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 2010-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,19 +30,19 @@ * @author Costin Leau * @author Stephane Nicoll */ -public class NoOpCacheManagerTests { +class NoOpCacheManagerTests { private final CacheManager manager = new NoOpCacheManager(); @Test - public void testGetCache() throws Exception { + void testGetCache() { Cache cache = this.manager.getCache("bucket"); assertThat(cache).isNotNull(); assertThat(this.manager.getCache("bucket")).isSameAs(cache); } @Test - public void testNoOpCache() throws Exception { + void testNoOpCache() { String name = createRandomKey(); Cache cache = this.manager.getCache(name); assertThat(cache.getName()).isEqualTo(name); @@ -54,15 +54,15 @@ public void testNoOpCache() throws Exception { } @Test - public void testCacheName() throws Exception { + void testCacheName() { String name = "bucket"; - assertThat(this.manager.getCacheNames().contains(name)).isFalse(); + assertThat(this.manager.getCacheNames()).doesNotContain(name); this.manager.getCache(name); - assertThat(this.manager.getCacheNames().contains(name)).isTrue(); + assertThat(this.manager.getCacheNames()).contains(name); } @Test - public void testCacheCallable() throws Exception { + void testCacheCallable() { String name = createRandomKey(); Cache cache = this.manager.getCache(name); Object returnValue = new Object(); @@ -71,7 +71,7 @@ public void testCacheCallable() throws Exception { } @Test - public void testCacheGetCallableFail() { + void testCacheGetCallableFail() { 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 6f78f96b781c..f1120e87c03c 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -41,240 +39,237 @@ * @author Stephane Nicoll * @author Sam Brannen */ -public class AnnotationCacheOperationSourceTests { +class AnnotationCacheOperationSourceTests { private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); @Test - public void singularAnnotation() { + void singularAnnotation() { Collection ops = getOps(AnnotatedClass.class, "singular", 1); - assertThat(ops.iterator().next()).isInstanceOf(CacheableOperation.class); + assertThat(ops).singleElement().satisfies(cacheOperation(CacheableOperation.class, "test")); } @Test - public void multipleAnnotation() { + void multipleAnnotation() { Collection ops = getOps(AnnotatedClass.class, "multiple", 2); - Iterator it = ops.iterator(); - assertThat(it.next()).isInstanceOf(CacheableOperation.class); - assertThat(it.next()).isInstanceOf(CacheEvictOperation.class); + assertThat(ops).satisfiesExactly(cacheOperation(CacheableOperation.class), + cacheOperation(CacheEvictOperation.class)); } @Test - public void caching() { + void caching() { Collection ops = getOps(AnnotatedClass.class, "caching", 2); - Iterator it = ops.iterator(); - assertThat(it.next()).isInstanceOf(CacheableOperation.class); - assertThat(it.next()).isInstanceOf(CacheEvictOperation.class); + assertThat(ops).satisfiesExactly(cacheOperation(CacheableOperation.class), + cacheOperation(CacheEvictOperation.class)); } @Test - public void emptyCaching() { + void emptyCaching() { getOps(AnnotatedClass.class, "emptyCaching", 0); } @Test - public void singularStereotype() { + void singularStereotype() { Collection ops = getOps(AnnotatedClass.class, "singleStereotype", 1); - assertThat(ops.iterator().next()).isInstanceOf(CacheEvictOperation.class); + assertThat(ops).satisfiesExactly(cacheOperation(CacheEvictOperation.class)); } @Test - public void multipleStereotypes() { + void multipleStereotypes() { Collection ops = getOps(AnnotatedClass.class, "multipleStereotype", 3); - Iterator it = ops.iterator(); - assertThat(it.next()).isInstanceOf(CacheableOperation.class); - CacheOperation next = it.next(); - assertThat(next).isInstanceOf(CacheEvictOperation.class); - assertThat(next.getCacheNames()).contains("foo"); - next = it.next(); - assertThat(next).isInstanceOf(CacheEvictOperation.class); - assertThat(next.getCacheNames()).contains("bar"); + assertThat(ops).satisfiesExactly(cacheOperation(CacheableOperation.class), + cacheOperation(CacheEvictOperation.class, "foo"), + cacheOperation(CacheEvictOperation.class, "bar") + ); } @Test - public void singleComposedAnnotation() { + void singleComposedAnnotation() { Collection ops = getOps(AnnotatedClass.class, "singleComposed", 2); - Iterator it = ops.iterator(); - - CacheOperation cacheOperation = it.next(); - assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); - assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("directly declared")); - assertThat(cacheOperation.getKey()).isEmpty(); - - cacheOperation = it.next(); - assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); - assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("composedCache")); - assertThat(cacheOperation.getKey()).isEqualTo("composedKey"); + assertThat(ops).satisfiesExactly( + zero -> { + assertThat(zero).satisfies(cacheOperation(CacheOperation.class, "directly declared")); + assertThat(zero.getKey()).isEmpty(); + }, + first -> { + assertThat(first).satisfies(cacheOperation(CacheOperation.class, "composedCache")); + assertThat(first.getKey()).isEqualTo("composedKey"); + } + ); } @Test - public void multipleComposedAnnotations() { + void multipleComposedAnnotations() { Collection ops = getOps(AnnotatedClass.class, "multipleComposed", 4); - Iterator it = ops.iterator(); - - CacheOperation cacheOperation = it.next(); - assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); - assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("directly declared")); - assertThat(cacheOperation.getKey()).isEmpty(); - - cacheOperation = it.next(); - assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); - assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("composedCache")); - assertThat(cacheOperation.getKey()).isEqualTo("composedKey"); - - cacheOperation = it.next(); - assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); - assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("foo")); - assertThat(cacheOperation.getKey()).isEmpty(); - - cacheOperation = it.next(); - assertThat(cacheOperation).isInstanceOf(CacheEvictOperation.class); - assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("composedCacheEvict")); - assertThat(cacheOperation.getKey()).isEqualTo("composedEvictionKey"); + assertThat(ops).satisfiesExactly( + zero -> { + assertThat(zero).satisfies(cacheOperation(CacheOperation.class, "directly declared")); + assertThat(zero.getKey()).isEmpty(); + }, + first -> { + assertThat(first).satisfies(cacheOperation(CacheOperation.class, "composedCache")); + assertThat(first.getKey()).isEqualTo("composedKey"); + }, + two -> { + assertThat(two).satisfies(cacheOperation(CacheOperation.class, "foo")); + assertThat(two.getKey()).isEmpty(); + }, + three -> { + assertThat(three).satisfies(cacheOperation(CacheEvictOperation.class, "composedCacheEvict")); + assertThat(three.getKey()).isEqualTo("composedEvictionKey"); + } + ); } @Test - public void customKeyGenerator() { + void customKeyGenerator() { Collection ops = getOps(AnnotatedClass.class, "customKeyGenerator", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation.getKeyGenerator()).as("Custom key generator not set").isEqualTo("custom"); + assertThat(ops).singleElement().satisfies(cacheOperation -> + assertThat(cacheOperation.getKeyGenerator()).isEqualTo("custom")); + } @Test - public void customKeyGeneratorInherited() { + void customKeyGeneratorInherited() { Collection ops = getOps(AnnotatedClass.class, "customKeyGeneratorInherited", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation.getKeyGenerator()).as("Custom key generator not set").isEqualTo("custom"); + assertThat(ops).singleElement().satisfies(cacheOperation -> + assertThat(cacheOperation.getKeyGenerator()).isEqualTo("custom")); } @Test - public void keyAndKeyGeneratorCannotBeSetTogether() { + void keyAndKeyGeneratorCannotBeSetTogether() { assertThatIllegalStateException().isThrownBy(() -> getOps(AnnotatedClass.class, "invalidKeyAndKeyGeneratorSet")); } @Test - public void customCacheManager() { + void customCacheManager() { Collection ops = getOps(AnnotatedClass.class, "customCacheManager", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation.getCacheManager()).as("Custom cache manager not set").isEqualTo("custom"); + assertThat(ops).singleElement().satisfies(cacheOperation -> + assertThat(cacheOperation.getCacheManager()).isEqualTo("custom")); } @Test - public void customCacheManagerInherited() { + void customCacheManagerInherited() { Collection ops = getOps(AnnotatedClass.class, "customCacheManagerInherited", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation.getCacheManager()).as("Custom cache manager not set").isEqualTo("custom"); + assertThat(ops).singleElement().satisfies(cacheOperation -> + assertThat(cacheOperation.getCacheManager()).isEqualTo("custom")); } @Test - public void customCacheResolver() { + void customCacheResolver() { Collection ops = getOps(AnnotatedClass.class, "customCacheResolver", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation.getCacheResolver()).as("Custom cache resolver not set").isEqualTo("custom"); + assertThat(ops).singleElement().satisfies(cacheOperation -> + assertThat(cacheOperation.getCacheResolver()).isEqualTo("custom")); } @Test - public void customCacheResolverInherited() { + void customCacheResolverInherited() { Collection ops = getOps(AnnotatedClass.class, "customCacheResolverInherited", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation.getCacheResolver()).as("Custom cache resolver not set").isEqualTo("custom"); + assertThat(ops).singleElement().satisfies(cacheOperation -> + assertThat(cacheOperation.getCacheResolver()).isEqualTo("custom")); } @Test - public void cacheResolverAndCacheManagerCannotBeSetTogether() { + void cacheResolverAndCacheManagerCannotBeSetTogether() { assertThatIllegalStateException().isThrownBy(() -> getOps(AnnotatedClass.class, "invalidCacheResolverAndCacheManagerSet")); } @Test - public void fullClassLevelWithCustomCacheName() { + void fullClassLevelWithCustomCacheName() { Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheName", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "classKeyGenerator", "", "classCacheResolver", "custom")); } @Test - public void fullClassLevelWithCustomKeyManager() { + void fullClassLevelWithCustomKeyManager() { Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelKeyGenerator", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "custom", "", "classCacheResolver" , "classCacheName"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "custom", "", "classCacheResolver" , "classCacheName")); } @Test - public void fullClassLevelWithCustomCacheManager() { + void fullClassLevelWithCustomCacheManager() { Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheManager", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "classKeyGenerator", "custom", "", "classCacheName")); } @Test - public void fullClassLevelWithCustomCacheResolver() { + void fullClassLevelWithCustomCacheResolver() { Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheResolver", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom" , "classCacheName"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "classKeyGenerator", "", "custom" , "classCacheName")); } @Test - public void validateNoCacheIsValid() { + void validateNoCacheIsValid() { // Valid as a CacheResolver might return the cache names to use with other info Collection ops = getOps(AnnotatedClass.class, "noCacheNameSpecified"); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation.getCacheNames()).as("cache names set must not be null").isNotNull(); - assertThat(cacheOperation.getCacheNames()).as("no cache names specified").isEmpty(); + assertThat(ops).singleElement().satisfies(cacheOperation -> + assertThat(cacheOperation.getCacheNames()).isEmpty()); + } @Test - public void customClassLevelWithCustomCacheName() { + void customClassLevelWithCustomCacheName() { Collection ops = getOps(AnnotatedClassWithCustomDefault.class, "methodLevelCacheName", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "classKeyGenerator", "", "classCacheResolver", "custom")); } @Test - public void severalCacheConfigUseClosest() { + void severalCacheConfigUseClosest() { Collection ops = getOps(MultipleCacheConfig.class, "multipleCacheConfig"); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "", "", "", "myCache"); + assertThat(ops).singleElement().satisfies(hasSharedConfig("", "", "", "myCache")); } @Test - public void cacheConfigFromInterface() { + void cacheConfigFromInterface() { Collection ops = getOps(InterfaceCacheConfig.class, "interfaceCacheConfig"); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "", "", "", "myCache"); + assertThat(ops).singleElement().satisfies(hasSharedConfig("", "", "", "myCache")); } @Test - public void cacheAnnotationOverride() { + void cacheAnnotationOverride() { Collection ops = getOps(InterfaceCacheConfig.class, "interfaceCacheableOverride"); - assertThat(ops.size()).isSameAs(1); - CacheOperation cacheOperation = ops.iterator().next(); - assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); + assertThat(ops).singleElement().satisfies(cacheOperation(CacheableOperation.class)); } @Test - public void partialClassLevelWithCustomCacheManager() { + void partialClassLevelWithCustomCacheManager() { Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheManager", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "classKeyGenerator", "custom", "", "classCacheName")); } @Test - public void partialClassLevelWithCustomCacheResolver() { + void partialClassLevelWithCustomCacheResolver() { Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheResolver", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom", "classCacheName"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "classKeyGenerator", "", "custom", "classCacheName")); } @Test - public void partialClassLevelWithNoCustomization() { + void partialClassLevelWithNoCustomization() { Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "noCustomization", 1); - CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classKeyGenerator", "classCacheManager", "", "classCacheName"); + assertThat(ops).singleElement().satisfies(hasSharedConfig( + "classKeyGenerator", "classCacheManager", "", "classCacheName")); + } + + private Consumer cacheOperation(Class type, String... cacheNames) { + return candidate -> { + assertThat(candidate).isInstanceOf(type); + assertThat(candidate.getCacheNames()).containsExactly(cacheNames); + }; } + private Consumer cacheOperation(Class type) { + return candidate -> assertThat(candidate).isInstanceOf(type); + } private Collection getOps(Class target, String name, int expectedNumberOfOperations) { Collection result = getOps(target, name); @@ -292,14 +287,15 @@ private Collection getOps(Class target, String name) { } } - private void assertSharedConfig(CacheOperation actual, String keyGenerator, String cacheManager, + private Consumer hasSharedConfig(String keyGenerator, String cacheManager, String cacheResolver, String... cacheNames) { - - assertThat(actual.getKeyGenerator()).as("Wrong key manager").isEqualTo(keyGenerator); - assertThat(actual.getCacheManager()).as("Wrong cache manager").isEqualTo(cacheManager); - assertThat(actual.getCacheResolver()).as("Wrong cache resolver").isEqualTo(cacheResolver); - assertThat(actual.getCacheNames()).as("Wrong number of cache names").hasSameSizeAs(cacheNames); - Arrays.stream(cacheNames).forEach(cacheName -> assertThat(actual.getCacheNames().contains(cacheName)).as("Cache '" + cacheName + "' not found in " + actual.getCacheNames()).isTrue()); + return actual -> { + assertThat(actual.getKeyGenerator()).isEqualTo(keyGenerator); + assertThat(actual.getCacheManager()).isEqualTo(cacheManager); + assertThat(actual.getCacheResolver()).isEqualTo(cacheResolver); + assertThat(actual.getCacheNames()).hasSameSizeAs(cacheNames); + assertThat(actual.getCacheNames()).containsExactly(cacheNames); + }; } 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 new file mode 100644 index 000000000000..6adf6a2ebf0e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java @@ -0,0 +1,331 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache.annotation; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicLong; + +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 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.AssertionsForClassTypes.catchThrowable; + +/** + * Tests for annotation-based caching methods that use reactive operators. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 6.1 + */ +class ReactiveCachingTests { + + @ParameterizedTest + @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, + EarlyCacheHitDeterminationWithoutNullValuesConfig.class, + LateCacheHitDeterminationConfig.class, + LateCacheHitDeterminationWithValueWrapperConfig.class}) + void cacheHitDetermination(Class configClass) { + + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Object key = new Object(); + + Long r1 = service.cacheFuture(key).join(); + Long r2 = service.cacheFuture(key).join(); + Long r3 = service.cacheFuture(key).join(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isSameAs(r2).isSameAs(r3); + + key = new Object(); + + r1 = service.cacheMono(key).block(); + r2 = service.cacheMono(key).block(); + r3 = service.cacheMono(key).block(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isSameAs(r2).isSameAs(r3); + + key = new Object(); + + r1 = service.cacheFlux(key).blockFirst(); + r2 = service.cacheFlux(key).blockFirst(); + r3 = service.cacheFlux(key).blockFirst(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isSameAs(r2).isSameAs(r3); + + key = new Object(); + + List l1 = service.cacheFlux(key).collectList().block(); + List l2 = service.cacheFlux(key).collectList().block(); + List l3 = service.cacheFlux(key).collectList().block(); + + assertThat(l1).isNotNull(); + assertThat(l1).as("cacheFlux collectList").isEqualTo(l2).isEqualTo(l3); + + key = new Object(); + + r1 = service.cacheMono(key).block(); + r2 = service.cacheMono(key).block(); + r3 = service.cacheMono(key).block(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono common key").isSameAs(r2).isSameAs(r3); + + // Same key as for Mono, reusing its cached value + + r1 = service.cacheFlux(key).blockFirst(); + r2 = service.cacheFlux(key).blockFirst(); + r3 = service.cacheFlux(key).blockFirst(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst common key").isSameAs(r2).isSameAs(r3); + + ctx.close(); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Object key = new Object(); + Long r1 = service.cacheFuture(key).join(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isEqualTo(0L); + + key = new Object(); + + r1 = service.cacheMono(key).block(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isEqualTo(1L); + + key = new Object(); + + r1 = service.cacheFlux(key).blockFirst(); + + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } + + @Test + void cacheErrorHandlerWithSimpleCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); + assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class) + .extracting(Throwable::getCause) + .isInstanceOf(UnsupportedOperationException.class); + + Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block()); + assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class); + + Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst()); + assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class); + } + + @ParameterizedTest + @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, + EarlyCacheHitDeterminationWithoutNullValuesConfig.class, + LateCacheHitDeterminationConfig.class, + LateCacheHitDeterminationWithValueWrapperConfig.class}) + void fluxCacheDoesntDependOnFirstRequest(Class configClass) { + + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Object key = new Object(); + + List l1 = service.cacheFlux(key).take(1L, true).collectList().block(); + List l2 = service.cacheFlux(key).take(3L, true).collectList().block(); + List l3 = service.cacheFlux(key).collectList().block(); + + Long first = l1.get(0); + + assertThat(l1).as("l1").containsExactly(first); + assertThat(l2).as("l2").containsExactly(first, 0L, -1L); + assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L); + + ctx.close(); + } + + @CacheConfig(cacheNames = "first") + static class ReactiveCacheableService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable + CompletableFuture cacheFuture(Object arg) { + return CompletableFuture.completedFuture(this.counter.getAndIncrement()); + } + + @Cacheable + Mono cacheMono(Object arg) { + // here counter not only reflects invocations of cacheMono but subscriptions to + // the returned Mono as well. See https://github.com/spring-projects/spring-framework/issues/32370 + return Mono.defer(() -> Mono.just(this.counter.getAndIncrement())); + } + + @Cacheable + Flux cacheFlux(Object arg) { + // here counter not only reflects invocations of cacheFlux but subscriptions to + // the returned Flux as well. See https://github.com/spring-projects/spring-framework/issues/32370 + return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L)); + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class EarlyCacheHitDeterminationConfig { + + @Bean + CacheManager cacheManager() { + return new ConcurrentMapCacheManager("first"); + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class EarlyCacheHitDeterminationWithoutNullValuesConfig { + + @Bean + CacheManager cacheManager() { + ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("first"); + cm.setAllowNullValues(false); + return cm; + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class LateCacheHitDeterminationConfig { + + @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.completedFuture(lookup(key)); + } + @Override + public void put(Object key, @Nullable Object value) { + assertThat(get(key)).as("Double put").isNull(); + super.put(key, value); + } + }; + } + }; + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class LateCacheHitDeterminationWithValueWrapperConfig { + + @Bean + CacheManager cacheManager() { + return new ConcurrentMapCacheManager("first") { + @Override + protected Cache createConcurrentMapCache(String name) { + return new ConcurrentMapCache(name, isAllowNullValues()) { + @Override + public CompletableFuture retrieve(Object key) { + Object value = lookup(key); + return CompletableFuture.completedFuture(value != null ? toValueWrapper(value) : null); + } + @Override + public void put(Object key, @Nullable Object value) { + assertThat(get(key)).as("Double put").isNull(); + super.put(key, 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.supplyAsync(() -> { + throw 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 70d9254dcc36..5a8baa9eb642 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,10 @@ * @author Juergen Hoeller * @author Stephane Nicoll */ -public class ConcurrentMapCacheManagerTests { +class ConcurrentMapCacheManagerTests { @Test - public void testDynamicMode() { + void testDynamicMode() { CacheManager cm = new ConcurrentMapCacheManager(); Cache cache1 = cm.getCache("c1"); assertThat(cache1).isInstanceOf(ConcurrentMapCache.class); @@ -68,7 +68,7 @@ public void testDynamicMode() { } @Test - public void testStaticMode() { + void testStaticMode() { ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2"); Cache cache1 = cm.getCache("c1"); assertThat(cache1).isInstanceOf(ConcurrentMapCache.class); @@ -115,7 +115,7 @@ public void testStaticMode() { } @Test - public void testChangeStoreByValue() { + void testChangeStoreByValue() { 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 ac7d52b59eb0..533423e50ea5 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ * @author Juergen Hoeller * @author Stephane Nicoll */ -public class ConcurrentMapCacheTests extends AbstractValueAdaptingCacheTests { +class ConcurrentMapCacheTests extends AbstractValueAdaptingCacheTests { protected ConcurrentMap nativeCache; @@ -48,7 +48,7 @@ public class ConcurrentMapCacheTests extends AbstractValueAdaptingCacheTests(); this.cache = new ConcurrentMapCache(CACHE_NAME, this.nativeCache, true); this.nativeCacheNoNull = new ConcurrentHashMap<>(); @@ -63,7 +63,7 @@ protected ConcurrentMapCache getCache() { @Override protected ConcurrentMapCache getCache(boolean allowNull) { - return allowNull ? this.cache : this.cacheNoNull; + return (allowNull ? this.cache : this.cacheNoNull); } @Override @@ -73,13 +73,13 @@ protected ConcurrentMap getNativeCache() { @Test - public void testIsStoreByReferenceByDefault() { + void testIsStoreByReferenceByDefault() { assertThat(this.cache.isStoreByValue()).isFalse(); } @SuppressWarnings("unchecked") @Test - public void testSerializer() { + void testSerializer() { ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); assertThat(serializeCache.isStoreByValue()).isTrue(); @@ -93,7 +93,7 @@ public void testSerializer() { } @Test - public void testNonSerializableContent() { + void testNonSerializableContent() { ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); assertThatIllegalArgumentException().isThrownBy(() -> @@ -104,7 +104,7 @@ public void testNonSerializableContent() { } @Test - public void testInvalidSerializedContent() { + void testInvalidSerializedContent() { 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 7940c65b25d5..92122bee02eb 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-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ * @author Costin Leau * @author Chris Beams */ -public class AnnotationDrivenCacheConfigTests extends AbstractCacheAnnotationTests { +class AnnotationDrivenCacheConfigTests extends AbstractCacheAnnotationTests { @Override protected ConfigurableApplicationContext getApplicationContext() { 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 230337961b8f..a0c2d5111d06 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ * @author Chris Beams * @author Stephane Nicoll */ -public class AnnotationNamespaceDrivenTests extends AbstractCacheAnnotationTests { +class AnnotationNamespaceDrivenTests extends AbstractCacheAnnotationTests { @Override protected ConfigurableApplicationContext getApplicationContext() { @@ -40,14 +40,14 @@ protected ConfigurableApplicationContext getApplicationContext() { } @Test - public void testKeyStrategy() { + void testKeyStrategy() { CacheInterceptor ci = this.ctx.getBean( "org.springframework.cache.interceptor.CacheInterceptor#0", CacheInterceptor.class); assertThat(ci.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator")); } @Test - public void cacheResolver() { + void cacheResolver() { ConfigurableApplicationContext context = new GenericXmlApplicationContext( "/org/springframework/cache/config/annotationDrivenCacheNamespace-resolver.xml"); @@ -57,7 +57,7 @@ public void cacheResolver() { } @Test - public void bothSetOnlyResolverIsUsed() { + void bothSetOnlyResolverIsUsed() { ConfigurableApplicationContext context = new GenericXmlApplicationContext( "/org/springframework/cache/config/annotationDrivenCacheNamespace-manager-resolver.xml"); @@ -67,7 +67,7 @@ public void bothSetOnlyResolverIsUsed() { } @Test - public void testCacheErrorHandler() { + void testCacheErrorHandler() { 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 f15aa696f16c..d1888ec84baa 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ * @author Costin Leau * @author Chris Beams */ -public class CacheAdviceNamespaceTests extends AbstractCacheAnnotationTests { +class CacheAdviceNamespaceTests extends AbstractCacheAnnotationTests { @Override protected ConfigurableApplicationContext getApplicationContext() { @@ -38,7 +38,7 @@ protected ConfigurableApplicationContext getApplicationContext() { } @Test - public void testKeyStrategy() { + void testKeyStrategy() { 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/CustomInterceptorTests.java b/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java index bee527391c22..c3a84002e588 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,13 +49,13 @@ class CustomInterceptorTests { protected CacheableService cs; @BeforeEach - public void setup() { + void setup() { this.ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class); this.cs = ctx.getBean("service", CacheableService.class); } @AfterEach - public void tearDown() { + void tearDown() { this.ctx.close(); } 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 f4e25b70caf8..0c3b2181eb21 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 @@ -54,7 +54,7 @@ class EnableCachingIntegrationTests { @AfterEach - public void closeContext() { + void closeContext() { if (this.context != null) { this.context.close(); } 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 3b2c22ebb572..28475925ba5e 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,16 @@ package org.springframework.cache.config; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.CacheErrorHandler; @@ -139,6 +144,18 @@ void bothSetOnlyResolverIsUsed() { context.close(); } + @Test + void mutableKey() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EnableCachingConfig.class, ServiceWithMutableKey.class); + ctx.refresh(); + + ServiceWithMutableKey service = ctx.getBean(ServiceWithMutableKey.class); + String result = service.find(new ArrayList<>(List.of("id"))); + assertThat(service.find(new ArrayList<>(List.of("id")))).isSameAs(result); + ctx.close(); + } + @Configuration @EnableCaching @@ -277,4 +294,14 @@ public CacheResolver cacheResolver() { } } + + static class ServiceWithMutableKey { + + @Cacheable(value = "testCache", keyGenerator = "customKeyGenerator") + public String find(Collection id) { + id.add("other"); + return id.toString(); + } + } + } 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 5ba071381fc7..0489d30ba694 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ /** * @author Stephane Nicoll */ -public class ExpressionCachingIntegrationTests { +class ExpressionCachingIntegrationTests { @Test // SPR-11692 @SuppressWarnings("unchecked") 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 8593ba54b6bd..ade3d831b824 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 @@ -60,6 +60,7 @@ class CacheErrorHandlerTests { private SimpleService simpleService; + @BeforeEach void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); @@ -69,11 +70,13 @@ void setup() { this.simpleService = context.getBean(SimpleService.class); } + @AfterEach - void tearDown() { + void closeContext() { this.context.close(); } + @Test void getFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); @@ -107,9 +110,9 @@ void getFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.get(0L)) - .withMessage("Test exception on get"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.get(0L)) + .withMessage("Test exception on get"); } @Test @@ -128,9 +131,9 @@ void putFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.put(0L)) - .withMessage("Test exception on put"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.put(0L)) + .withMessage("Test exception on put"); } @Test @@ -149,9 +152,9 @@ void evictFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.evict(0L)) - .withMessage("Test exception on evict"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.evict(0L)) + .withMessage("Test exception on evict"); } @Test @@ -170,9 +173,9 @@ void clearFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.clear()) - .withMessage("Test exception on clear"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.clear()) + .withMessage("Test exception on clear"); } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java similarity index 82% rename from spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java rename to spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java index c6be614ff46b..11625dce481e 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,23 +31,31 @@ import org.springframework.cache.annotation.Caching; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.support.StaticApplicationContext; 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; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** + * Tests for {@link CacheOperationExpressionEvaluator}. + * * @author Costin Leau * @author Phillip Webb * @author Sam Brannen * @author Stephane Nicoll */ -public class ExpressionEvaluatorTests { +class CacheOperationExpressionEvaluatorTests { + + private final StandardEvaluationContext originalEvaluationContext = new StandardEvaluationContext(); - private final CacheOperationExpressionEvaluator eval = new CacheOperationExpressionEvaluator(); + private final CacheOperationExpressionEvaluator eval = new CacheOperationExpressionEvaluator( + new CacheEvaluationContextFactory(this.originalEvaluationContext)); private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); @@ -59,22 +67,22 @@ private Collection getOps(String name) { @Test - public void testMultipleCachingSource() { + void testMultipleCachingSource() { Collection ops = getOps("multipleCaching"); assertThat(ops).hasSize(2); Iterator it = ops.iterator(); CacheOperation next = it.next(); assertThat(next).isInstanceOf(CacheableOperation.class); - assertThat(next.getCacheNames().contains("test")).isTrue(); + assertThat(next.getCacheNames()).contains("test"); assertThat(next.getKey()).isEqualTo("#a"); next = it.next(); assertThat(next).isInstanceOf(CacheableOperation.class); - assertThat(next.getCacheNames().contains("test")).isTrue(); + assertThat(next.getCacheNames()).contains("test"); assertThat(next.getKey()).isEqualTo("#b"); } @Test - public void testMultipleCachingEval() { + void testMultipleCachingEval() { AnnotatedClass target = new AnnotatedClass(); Method method = ReflectionUtils.findMethod( AnnotatedClass.class, "multipleCaching", Object.class, Object.class); @@ -82,7 +90,7 @@ public void testMultipleCachingEval() { Collection caches = Collections.singleton(new ConcurrentMapCache("test")); EvaluationContext evalCtx = this.eval.createEvaluationContext(caches, method, args, - target, target.getClass(), method, CacheOperationExpressionEvaluator.NO_RESULT, null); + target, target.getClass(), method, CacheOperationExpressionEvaluator.NO_RESULT); Collection ops = getOps("multipleCaching"); Iterator it = ops.iterator(); @@ -96,36 +104,36 @@ public void testMultipleCachingEval() { } @Test - public void withReturnValue() { + void withReturnValue() { EvaluationContext context = createEvaluationContext("theResult"); Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); assertThat(value).isEqualTo("theResult"); } @Test - public void withNullReturn() { + void withNullReturn() { EvaluationContext context = createEvaluationContext(null); Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); assertThat(value).isNull(); } @Test - public void withoutReturnValue() { + void withoutReturnValue() { EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.NO_RESULT); Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); assertThat(value).isNull(); } @Test - public void unavailableReturnValue() { + void unavailableReturnValue() { EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE); - assertThatExceptionOfType(VariableNotAvailableException.class).isThrownBy(() -> - new SpelExpressionParser().parseExpression("#result").getValue(context)) - .satisfies(ex -> assertThat(ex.getName()).isEqualTo("result")); + assertThatExceptionOfType(VariableNotAvailableException.class) + .isThrownBy(() -> new SpelExpressionParser().parseExpression("#result").getValue(context)) + .withMessage("Variable 'result' not available"); } @Test - public void resolveBeanReference() { + void resolveBeanReference() { StaticApplicationContext applicationContext = new StaticApplicationContext(); BeanDefinition beanDefinition = new RootBeanDefinition(String.class); applicationContext.registerBeanDefinition("myBean", beanDefinition); @@ -140,14 +148,17 @@ private EvaluationContext createEvaluationContext(Object result) { return createEvaluationContext(result, null); } - private EvaluationContext createEvaluationContext(Object result, BeanFactory beanFactory) { + private EvaluationContext createEvaluationContext(Object result, @Nullable BeanFactory beanFactory) { + if (beanFactory != null) { + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } AnnotatedClass target = new AnnotatedClass(); Method method = ReflectionUtils.findMethod( AnnotatedClass.class, "multipleCaching", Object.class, Object.class); Object[] args = new Object[] {new Object(), new Object()}; Collection caches = Collections.singleton(new ConcurrentMapCache("test")); return this.eval.createEvaluationContext( - caches, method, args, target, target.getClass(), method, result, beanFactory); + 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 a76ec69e8db2..9c2b5c1ba3ca 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-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,10 @@ * @author John Blum * @author Juergen Hoeller */ -public class CacheProxyFactoryBeanTests { +class CacheProxyFactoryBeanTests { @Test - public void configurationClassWithCacheProxyFactoryBean() { + void configurationClassWithCacheProxyFactoryBean() { try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(CacheProxyFactoryBeanConfiguration.class)) { Greeter greeter = applicationContext.getBean("greeter", Greeter.class); 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 f80f6b8461c1..c47526d30336 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ * * @author Stephane Nicoll */ -public class CachePutEvaluationTests { +class CachePutEvaluationTests { private ConfigurableApplicationContext context; @@ -51,22 +51,22 @@ public class CachePutEvaluationTests { private SimpleService service; + @BeforeEach - public void setup() { + void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); this.cache = this.context.getBean(CacheManager.class).getCache("test"); this.service = this.context.getBean(SimpleService.class); } @AfterEach - public void close() { - if (this.context != null) { - this.context.close(); - } + void closeContext() { + this.context.close(); } + @Test - public void mutualGetPutExclusion() { + void mutualGetPutExclusion() { String key = "1"; Long first = this.service.getOrPut(key, true); @@ -83,7 +83,7 @@ public void mutualGetPutExclusion() { } @Test - public void getAndPut() { + void getAndPut() { this.cache.clear(); long key = 1; 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 51200a4408a8..4e5cafd54b9d 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ void setup() { } @AfterEach - void tearDown() { + void closeContext() { this.context.close(); } @@ -142,16 +142,17 @@ void namedResolution() { @Test void noCacheResolved() { Method method = ReflectionUtils.findMethod(SimpleService.class, "noCacheResolved", Object.class); - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.noCacheResolved(new Object())) - .withMessageContaining(method.toString()); + + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.noCacheResolved(new Object())) + .withMessageContaining(method.toString()); } @Test void unknownCacheResolver() { - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - this.simpleService.unknownCacheResolver(new Object())) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("unknownCacheResolver")); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.simpleService.unknownCacheResolver(new Object())) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("unknownCacheResolver")); } 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 56b22970d1ac..187379b39758 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ * @author Stephane Nicoll * @since 4.3 */ -public class CacheSyncFailureTests { +class CacheSyncFailureTests { private ConfigurableApplicationContext context; @@ -50,13 +50,13 @@ public class CacheSyncFailureTests { @BeforeEach - public void setup() { + void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); this.simpleService = this.context.getBean(SimpleService.class); } @AfterEach - public void closeContext() { + void closeContext() { if (this.context != null) { this.context.close(); } @@ -64,35 +64,35 @@ public void closeContext() { @Test - public void unlessSync() { + void unlessSync() { assertThatIllegalStateException() .isThrownBy(() -> this.simpleService.unlessSync("key")) .withMessageContaining("A sync=true operation does not support the unless attribute"); } @Test - public void severalCachesSync() { + void severalCachesSync() { assertThatIllegalStateException() .isThrownBy(() -> this.simpleService.severalCachesSync("key")) .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test - public void severalCachesWithResolvedSync() { + void severalCachesWithResolvedSync() { assertThatIllegalStateException() .isThrownBy(() -> this.simpleService.severalCachesWithResolvedSync("key")) .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test - public void syncWithAnotherOperation() { + void syncWithAnotherOperation() { assertThatIllegalStateException() .isThrownBy(() -> this.simpleService.syncWithAnotherOperation("key")) .withMessageContaining("A sync=true operation cannot be combined with other cache operations"); } @Test - public void syncWithTwoGetOperations() { + void syncWithTwoGetOperations() { assertThatIllegalStateException() .isThrownBy(() -> this.simpleService.syncWithTwoGetOperations("key")) .withMessageContaining("Only one sync=true operation is allowed"); 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 2d9398ccb1a1..06ff77d58a4f 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-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,12 @@ package org.springframework.cache.interceptor; +import java.lang.reflect.Method; + import org.junit.jupiter.api.Test; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -28,14 +31,15 @@ * @author Phillip Webb * @author Stephane Nicoll * @author Juergen Hoeller + * @author Sebastien Deleuze */ -public class SimpleKeyGeneratorTests { +class SimpleKeyGeneratorTests { private final SimpleKeyGenerator generator = new SimpleKeyGenerator(); @Test - public void noValues() { + void noValues() { Object k1 = generateKey(new Object[] {}); Object k2 = generateKey(new Object[] {}); Object k3 = generateKey(new Object[] { "different" }); @@ -46,7 +50,7 @@ public void noValues() { } @Test - public void singleValue() { + void singleValue() { Object k1 = generateKey(new Object[] { "a" }); Object k2 = generateKey(new Object[] { "a" }); Object k3 = generateKey(new Object[] { "different" }); @@ -58,7 +62,7 @@ public void singleValue() { } @Test - public void multipleValues() { + void multipleValues() { Object k1 = generateKey(new Object[] { "a", 1, "b" }); Object k2 = generateKey(new Object[] { "a", 1, "b" }); Object k3 = generateKey(new Object[] { "b", 1, "a" }); @@ -69,7 +73,7 @@ public void multipleValues() { } @Test - public void singleNullValue() { + void singleNullValue() { Object k1 = generateKey(new Object[] { null }); Object k2 = generateKey(new Object[] { null }); Object k3 = generateKey(new Object[] { "different" }); @@ -81,7 +85,7 @@ public void singleNullValue() { } @Test - public void multipleNullValues() { + void multipleNullValues() { Object k1 = generateKey(new Object[] { "a", null, "b", null }); Object k2 = generateKey(new Object[] { "a", null, "b", null }); Object k3 = generateKey(new Object[] { "a", null, "b" }); @@ -92,7 +96,7 @@ public void multipleNullValues() { } @Test - public void plainArray() { + void plainArray() { Object k1 = generateKey(new Object[] { new String[]{"a", "b"} }); Object k2 = generateKey(new Object[] { new String[]{"a", "b"} }); Object k3 = generateKey(new Object[] { new String[]{"b", "a"} }); @@ -103,7 +107,7 @@ public void plainArray() { } @Test - public void arrayWithExtraParameter() { + void arrayWithExtraParameter() { Object k1 = generateKey(new Object[] { new String[]{"a", "b"}, "c" }); Object k2 = generateKey(new Object[] { new String[]{"a", "b"}, "c" }); Object k3 = generateKey(new Object[] { new String[]{"b", "a"}, "c" }); @@ -114,7 +118,7 @@ public void arrayWithExtraParameter() { } @Test - public void serializedKeys() throws Exception { + void serializedKeys() throws Exception { Object k1 = SerializationTestUtils.serializeAndDeserialize(generateKey(new Object[] { "a", 1, "b" })); Object k2 = SerializationTestUtils.serializeAndDeserialize(generateKey(new Object[] { "a", 1, "b" })); Object k3 = SerializationTestUtils.serializeAndDeserialize(generateKey(new Object[] { "b", 1, "a" })); @@ -126,7 +130,8 @@ public void serializedKeys() throws Exception { private Object generateKey(Object[] arguments) { - return this.generator.generate(null, null, arguments); + Method method = ReflectionUtils.findMethod(this.getClass(), "generateKey", Object[].class); + return this.generator.generate(this, method, arguments); } } 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 cdc746be3fbe..5fe8db871345 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,11 +33,11 @@ public abstract class AbstractCircularImportDetectionTests { protected abstract ConfigurationClassParser newParser(); - protected abstract String loadAsConfigurationSource(Class clazz) throws Exception; + protected abstract String loadAsConfigurationSource(Class clazz); @Test - public void simpleCircularImportIsDetected() throws Exception { + void simpleCircularImportIsDetected() throws Exception { boolean threw = false; try { newParser().parse(loadAsConfigurationSource(A.class), "A"); @@ -52,7 +52,7 @@ public void simpleCircularImportIsDetected() throws Exception { } @Test - public void complexCircularImportIsDetected() throws Exception { + void complexCircularImportIsDetected() throws Exception { boolean threw = false; try { newParser().parse(loadAsConfigurationSource(X.class), "X"); 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 1acfc113d1d3..46750e613feb 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,10 +34,10 @@ * @author Andy Wilkinson * @author Liu Dongmiao */ -public class AggressiveFactoryBeanInstantiationTests { +class AggressiveFactoryBeanInstantiationTests { @Test - public void directlyRegisteredFactoryBean() { + void directlyRegisteredFactoryBean() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { context.register(SimpleFactoryBean.class); context.addBeanFactoryPostProcessor(factory -> @@ -48,7 +48,7 @@ public void directlyRegisteredFactoryBean() { } @Test - public void beanMethodFactoryBean() { + void beanMethodFactoryBean() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { context.register(BeanMethodConfiguration.class); context.addBeanFactoryPostProcessor(factory -> @@ -59,7 +59,7 @@ public void beanMethodFactoryBean() { } @Test - public void checkLinkageError() { + void checkLinkageError() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { context.register(BeanMethodConfigurationWithExceptionInInitializer.class); context.refresh(); 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 2722b821f2f6..884bc07d7869 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,23 +20,30 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +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; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +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.annotation.AliasFor; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for {@link AnnotationBeanNameGenerator}. + * Tests for {@link AnnotationBeanNameGenerator}. * * @author Rick Evans * @author Juergen Hoeller @@ -46,95 +53,139 @@ */ class AnnotationBeanNameGeneratorTests { - private AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); + private final BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + + private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); @Test - void generateBeanNameWithNamedComponent() { - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentWithName.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); - assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); - assertThat(beanName).isEqualTo("walden"); + void buildDefaultBeanName() { + BeanDefinition bd = annotatedBeanDef(ComponentFromNonStringMeta.class); + assertThat(this.beanNameGenerator.buildDefaultBeanName(bd, this.registry)) + .isEqualTo("annotationBeanNameGeneratorTests.ComponentFromNonStringMeta"); } @Test - void generateBeanNameWithDefaultNamedComponent() { - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(DefaultNamedComponent.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); - assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); - assertThat(beanName).isEqualTo("thoreau"); + void generateBeanNameWithNamedComponent() { + assertGeneratedName(ComponentWithName.class, "walden"); } @Test void generateBeanNameWithNamedComponentWhereTheNameIsBlank() { - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentWithBlankName.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); - assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); - String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); - assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + assertGeneratedNameIsDefault(ComponentWithBlankName.class); + } + + @Test + void generateBeanNameForConventionBasedComponentWithDuplicateIdenticalNames() { + assertGeneratedName(ConventionBasedComponentWithDuplicateIdenticalNames.class, "myComponent"); + } + + @Test + void generateBeanNameForComponentWithDuplicateIdenticalNames() { + assertGeneratedName(ComponentWithDuplicateIdenticalNames.class, "myComponent"); + } + + @Test + void generateBeanNameForConventionBasedComponentWithConflictingNames() { + BeanDefinition bd = annotatedBeanDef(ConventionBasedComponentWithMultipleConflictingNames.class); + assertThatIllegalStateException() + .isThrownBy(() -> generateBeanName(bd)) + .withMessage("Stereotype annotations suggest inconsistent component names: '%s' versus '%s'", + "myComponent", "myService"); + } + + @Test + void generateBeanNameForComponentWithConflictingNames() { + BeanDefinition bd = annotatedBeanDef(ComponentWithMultipleConflictingNames.class); + assertThatIllegalStateException() + .isThrownBy(() -> generateBeanName(bd)) + .withMessage("Stereotype annotations suggest inconsistent component names: " + + List.of("myComponent", "myService")); + } + + @Test + 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"); } @Test void generateBeanNameWithAnonymousComponentYieldsGeneratedBeanName() { - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnonymousComponent.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); - assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); - String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); - assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + assertGeneratedNameIsDefault(AnonymousComponent.class); } @Test void generateBeanNameFromMetaComponentWithStringValue() { - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentFromStringMeta.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - assertThat(beanName).isEqualTo("henry"); + assertGeneratedName(ComponentFromStringMeta.class, "henry"); } @Test void generateBeanNameFromMetaComponentWithNonStringValue() { - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentFromNonStringMeta.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - assertThat(beanName).isEqualTo("annotationBeanNameGeneratorTests.ComponentFromNonStringMeta"); + assertGeneratedNameIsDefault(ComponentFromNonStringMeta.class); } - @Test + @Test // SPR-11360 void generateBeanNameFromComposedControllerAnnotationWithoutName() { - // SPR-11360 - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComposedControllerAnnotationWithoutName.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); - assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + assertGeneratedNameIsDefault(ComposedControllerAnnotationWithoutName.class); } - @Test + @Test // SPR-11360 void generateBeanNameFromComposedControllerAnnotationWithBlankName() { - // SPR-11360 - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComposedControllerAnnotationWithBlankName.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); - assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + assertGeneratedNameIsDefault(ComposedControllerAnnotationWithBlankName.class); } - @Test + @Test // SPR-11360 void generateBeanNameFromComposedControllerAnnotationWithStringValue() { - // SPR-11360 - BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); - AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition( - ComposedControllerAnnotationWithStringValue.class); - String beanName = this.beanNameGenerator.generateBeanName(bd, registry); - assertThat(beanName).isEqualTo("restController"); + assertGeneratedName(ComposedControllerAnnotationWithStringValue.class, "restController"); + } + + @Test // gh-31089 + void generateBeanNameFromStereotypeAnnotationWithStringArrayValueAndExplicitComponentNameAlias() { + assertGeneratedName(ControllerAdviceClass.class, "myControllerAdvice"); + } + + @Test // gh-31089 + void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitComponentNameAlias() { + assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice"); + } + + + private void assertGeneratedName(Class clazz, String expectedName) { + BeanDefinition bd = annotatedBeanDef(clazz); + assertThat(generateBeanName(bd)).isNotBlank().isEqualTo(expectedName); + } + + private void assertGeneratedNameIsDefault(Class clazz) { + BeanDefinition bd = annotatedBeanDef(clazz); + String expectedName = this.beanNameGenerator.buildDefaultBeanName(bd); + assertThat(generateBeanName(bd)).isNotBlank().isEqualTo(expectedName); + } + + private AnnotatedBeanDefinition annotatedBeanDef(Class clazz) { + return new AnnotatedGenericBeanDefinition(clazz); + } + + private String generateBeanName(BeanDefinition bd) { + return this.beanNameGenerator.generateBeanName(bd, registry); } @@ -146,6 +197,42 @@ private static class ComponentWithName { private static class ComponentWithBlankName { } + @Component("myComponent") + @Service("myComponent") + static class ComponentWithDuplicateIdenticalNames { + } + + @Component("myComponent") + @Service("myService") + static class ComponentWithMultipleConflictingNames { + } + + @Retention(RetentionPolicy.RUNTIME) + @Component + @interface ConventionBasedComponent1 { + // This intentionally convention-based. Please do not add @AliasFor. + // See gh-31093. + String value() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Component + @interface ConventionBasedComponent2 { + // This intentionally convention-based. Please do not add @AliasFor. + // See gh-31093. + String value() default ""; + } + + @ConventionBasedComponent1("myComponent") + @ConventionBasedComponent2("myComponent") + static class ConventionBasedComponentWithDuplicateIdenticalNames { + } + + @ConventionBasedComponent1("myComponent") + @ConventionBasedComponent2("myService") + static class ConventionBasedComponentWithMultipleConflictingNames { + } + @Component private static class AnonymousComponent { } @@ -173,7 +260,8 @@ private static class ComponentFromNonStringMeta { @Target(ElementType.TYPE) @Controller @interface TestRestController { - + // This intentionally convention-based. Please do not add @AliasFor. + // See gh-31093. String value() default ""; } @@ -189,4 +277,55 @@ static class ComposedControllerAnnotationWithBlankName { static class ComposedControllerAnnotationWithStringValue { } + /** + * Mock of {@code org.springframework.web.bind.annotation.ControllerAdvice}, + * which also has a {@code value} attribute that is NOT a {@code String} that + * is meant to be used for the component name. + *

Declares a custom {@link #name} that explicitly aliases {@link Component#value()}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @interface TestControllerAdvice { + + @AliasFor(annotation = Component.class, attribute = "value") + String name() default ""; + + @AliasFor("basePackages") + String[] value() default {}; + + @AliasFor("value") + String[] basePackages() default {}; + } + + /** + * Mock of {@code org.springframework.web.bind.annotation.RestControllerAdvice}, + * which also has a {@code value} attribute that is NOT a {@code String} that + * is meant to be used for the component name. + *

Declares a custom {@link #name} that explicitly aliases + * {@link TestControllerAdvice#name()} instead of {@link Component#value()}. + */ + @Retention(RetentionPolicy.RUNTIME) + @TestControllerAdvice + @interface TestRestControllerAdvice { + + @AliasFor(annotation = TestControllerAdvice.class) + String name() default ""; + + @AliasFor(annotation = TestControllerAdvice.class) + String[] value() default {}; + + @AliasFor(annotation = TestControllerAdvice.class) + String[] basePackages() default {}; + } + + + @TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice") + static class ControllerAdviceClass { + } + + @TestRestControllerAdvice(basePackages = "com.example", name = "myRestControllerAdvice") + static class RestControllerAdviceClass { + } + } 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 1eb25d791ec5..80f174db288f 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -307,8 +307,8 @@ void individualBeanWithNullReturningSupplier() { assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanC.class), "c")).isTrue(); assertThat(context.getBeansOfType(BeanA.class)).isEmpty(); - assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()).isSameAs(context.getBean(BeanB.class)); - assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()).isSameAs(context.getBean(BeanC.class)); + assertThat(context.getBeansOfType(BeanB.class).values()).singleElement().isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBeansOfType(BeanC.class).values()).singleElement().isSameAs(context.getBean(BeanC.class)); assertThatExceptionOfType(NoSuchBeanDefinitionException.class) .isThrownBy(() -> context.getBeanFactory().resolveNamedBean(BeanA.class)); @@ -409,15 +409,17 @@ void individualBeanWithFactoryBeanTypeAsTargetType() { bd2.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, Integer.class))); bd2.setLazyInit(true); context.registerBeanDefinition("fb2", bd2); - context.registerBeanDefinition("ip", new RootBeanDefinition(FactoryBeanInjectionPoints.class)); + RootBeanDefinition bd3 = new RootBeanDefinition(FactoryBeanInjectionPoints.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("ip", bd3); context.refresh(); + assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryBean).isSameAs(context.getBean("&fb1")); + assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryResult).isSameAs(context.getBean("fb1")); assertThat(context.getType("&fb1")).isEqualTo(GenericHolderFactoryBean.class); assertThat(context.getType("fb1")).isEqualTo(GenericHolder.class); assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); assertThat(context.getBeanNamesForType(GenericHolderFactoryBean.class)).hasSize(1); - assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryBean).isSameAs(context.getBean("&fb1")); - assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryResult).isSameAs(context.getBean("fb1")); } @Test @@ -425,7 +427,7 @@ void individualBeanWithUnresolvedFactoryBeanTypeAsTargetType() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); RootBeanDefinition bd1 = new RootBeanDefinition(); bd1.setBeanClass(GenericHolderFactoryBean.class); - bd1.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, Object.class))); + bd1.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, String.class))); bd1.setLazyInit(true); context.registerBeanDefinition("fb1", bd1); RootBeanDefinition bd2 = new RootBeanDefinition(); @@ -433,13 +435,19 @@ void individualBeanWithUnresolvedFactoryBeanTypeAsTargetType() { bd2.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, Integer.class))); bd2.setLazyInit(true); context.registerBeanDefinition("fb2", bd2); - context.registerBeanDefinition("ip", new RootBeanDefinition(FactoryResultInjectionPoint.class)); + RootBeanDefinition bd3 = new RootBeanDefinition(FactoryBeanInjectionPoints.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("ip", bd3); context.refresh(); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); assertThat(context.getType("&fb1")).isEqualTo(GenericHolderFactoryBean.class); assertThat(context.getType("fb1")).isEqualTo(GenericHolder.class); assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); - assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); + assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); + assertThat(context.getBeanProvider(ResolvableType.forClassWithGenerics(GenericHolder.class, String.class))) + .containsOnly(context.getBean("fb1")); } @Test @@ -453,14 +461,19 @@ void individualBeanWithFactoryBeanObjectTypeAsTargetType() { bd2.setBeanClass(UntypedFactoryBean.class); bd2.setTargetType(ResolvableType.forClassWithGenerics(GenericHolder.class, Integer.class)); context.registerBeanDefinition("fb2", bd2); - context.registerBeanDefinition("ip", new RootBeanDefinition(FactoryResultInjectionPoint.class)); + RootBeanDefinition bd3 = new RootBeanDefinition(FactoryResultInjectionPoint.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("ip", bd3); context.refresh(); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); assertThat(context.getType("&fb1")).isEqualTo(GenericHolderFactoryBean.class); assertThat(context.getType("fb1")).isEqualTo(GenericHolder.class); assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); assertThat(context.getBeanNamesForType(GenericHolderFactoryBean.class)).hasSize(1); - assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); + assertThat(context.getBeanProvider(ResolvableType.forClassWithGenerics(GenericHolder.class, String.class))) + .containsOnly(context.getBean("fb1")); } @Test @@ -480,6 +493,9 @@ void individualBeanWithFactoryBeanObjectTypeAsTargetTypeAndLazy() { assertThat(context.getType("fb")).isEqualTo(String.class); assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(1); assertThat(context.getBeanNamesForType(TypedFactoryBean.class)).hasSize(1); + assertThat(context.getBeanProvider(String.class)).containsOnly(context.getBean("fb", String.class)); + assertThat(context.getBeanProvider(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))) + .containsOnly(context.getBean("&fb")); } @Test 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 68137b61ca5c..fb3d59b5c593 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,20 +37,20 @@ import static org.springframework.context.annotation.ScopedProxyMode.TARGET_CLASS; /** - * Unit tests for {@link AnnotationScopeMetadataResolver}. + * Tests for {@link AnnotationScopeMetadataResolver}. * * @author Rick Evans * @author Chris Beams * @author Juergen Hoeller * @author Sam Brannen */ -public class AnnotationScopeMetadataResolverTests { +class AnnotationScopeMetadataResolverTests { private AnnotationScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver(); @Test - public void resolveScopeMetadataShouldNotApplyScopedProxyModeToSingleton() { + void resolveScopeMetadataShouldNotApplyScopedProxyModeToSingleton() { AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithSingletonScope.class); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); @@ -59,7 +59,7 @@ public void resolveScopeMetadataShouldNotApplyScopedProxyModeToSingleton() { } @Test - public void resolveScopeMetadataShouldApplyScopedProxyModeToPrototype() { + void resolveScopeMetadataShouldApplyScopedProxyModeToPrototype() { this.scopeMetadataResolver = new AnnotationScopeMetadataResolver(INTERFACES); AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithPrototypeScope.class); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); @@ -69,7 +69,7 @@ public void resolveScopeMetadataShouldApplyScopedProxyModeToPrototype() { } @Test - public void resolveScopeMetadataShouldReadScopedProxyModeFromAnnotation() { + void resolveScopeMetadataShouldReadScopedProxyModeFromAnnotation() { AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithScopedProxy.class); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); @@ -78,7 +78,7 @@ public void resolveScopeMetadataShouldReadScopedProxyModeFromAnnotation() { } @Test - public void customRequestScope() { + void customRequestScope() { AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithCustomRequestScope.class); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); @@ -87,7 +87,7 @@ public void customRequestScope() { } @Test - public void customRequestScopeViaAsm() throws IOException { + void customRequestScopeViaAsm() throws IOException { MetadataReaderFactory readerFactory = new SimpleMetadataReaderFactory(); MetadataReader reader = readerFactory.getMetadataReader(AnnotatedWithCustomRequestScope.class.getName()); AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(reader.getAnnotationMetadata()); @@ -98,7 +98,7 @@ public void customRequestScopeViaAsm() throws IOException { } @Test - public void customRequestScopeWithAttribute() { + void customRequestScopeWithAttribute() { AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition( AnnotatedWithCustomRequestScopeWithAttributeOverride.class); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); @@ -108,7 +108,7 @@ public void customRequestScopeWithAttribute() { } @Test - public void customRequestScopeWithAttributeViaAsm() throws IOException { + void customRequestScopeWithAttributeViaAsm() throws IOException { MetadataReaderFactory readerFactory = new SimpleMetadataReaderFactory(); MetadataReader reader = readerFactory.getMetadataReader(AnnotatedWithCustomRequestScopeWithAttributeOverride.class.getName()); AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(reader.getAnnotationMetadata()); @@ -119,13 +119,13 @@ public void customRequestScopeWithAttributeViaAsm() throws IOException { } @Test - public void ctorWithNullScopedProxyMode() { + void ctorWithNullScopedProxyMode() { assertThatIllegalArgumentException().isThrownBy(() -> new AnnotationScopeMetadataResolver(null)); } @Test - public void setScopeAnnotationTypeWithNullType() { + void setScopeAnnotationTypeWithNullType() { assertThatIllegalArgumentException().isThrownBy(() -> scopeMetadataResolver.setScopeAnnotationType(null)); } 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 a6ad85b8651a..a71cff29eb25 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-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ * * @author Chris Beams */ -public class AsmCircularImportDetectionTests extends AbstractCircularImportDetectionTests { +class AsmCircularImportDetectionTests extends AbstractCircularImportDetectionTests { @Override protected ConfigurationClassParser newParser() { @@ -42,7 +42,7 @@ protected ConfigurationClassParser newParser() { } @Override - protected String loadAsConfigurationSource(Class clazz) throws Exception { + protected String loadAsConfigurationSource(Class clazz) { return clazz.getName(); } 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 4127bb93eb09..4090d428878b 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ class BeanMethodMetadataTests { @Test - void providesBeanMethodBeanDefinition() throws Exception { + void providesBeanMethodBeanDefinition() { AnnotationConfigApplicationContext context= new AnnotationConfigApplicationContext(Conf.class); BeanDefinition beanDefinition = context.getBeanDefinition("myBean"); assertThat(beanDefinition).as("should provide AnnotatedBeanDefinition").isInstanceOf(AnnotatedBeanDefinition.class); 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 4e62e4212c5d..2d8dee4d5384 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ /** * Tests regarding overloading and overriding of bean methods. + * *

Related to SPR-6618. * * @author Chris Beams @@ -39,115 +40,125 @@ public class BeanMethodPolymorphismTests { @Test - public void beanMethodDetectedOnSuperClass() { + void beanMethodDetectedOnSuperClass() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + assertThat(ctx.getBean("testBean", BaseTestBean.class)).isNotNull(); } @Test - public void beanMethodOverriding() { + void beanMethodOverriding() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(OverridingConfig.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); } @Test - public void beanMethodOverridingOnASM() { + void beanMethodOverridingOnASM() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.registerBeanDefinition("config", new RootBeanDefinition(OverridingConfig.class.getName())); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); } @Test - public void beanMethodOverridingWithNarrowedReturnType() { + void beanMethodOverridingWithNarrowedReturnType() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(NarrowedOverridingConfig.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); } @Test - public void beanMethodOverridingWithNarrowedReturnTypeOnASM() { + void beanMethodOverridingWithNarrowedReturnTypeOnASM() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.registerBeanDefinition("config", new RootBeanDefinition(NarrowedOverridingConfig.class.getName())); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); } @Test - public void beanMethodOverloadingWithoutInheritance() { + void beanMethodOverloadingWithoutInheritance() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithOverloading.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getBean(String.class)).isEqualTo("regular"); } @Test - public void beanMethodOverloadingWithoutInheritanceAndExtraDependency() { + void beanMethodOverloadingWithoutInheritanceAndExtraDependency() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithOverloading.class); ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); } @Test - public void beanMethodOverloadingWithAdditionalMetadata() { + void beanMethodOverloadingWithAdditionalMetadata() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithOverloadingAndAdditionalMetadata.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("regular"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); } @Test - public void beanMethodOverloadingWithAdditionalMetadataButOtherMethodExecuted() { + void beanMethodOverloadingWithAdditionalMetadataButOtherMethodExecuted() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithOverloadingAndAdditionalMetadata.class); ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); } @Test - public void beanMethodOverloadingWithInheritance() { + void beanMethodOverloadingWithInheritance() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(SubConfig.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); } - // SPR-11025 - @Test - public void beanMethodOverloadingWithInheritanceAndList() { + @Test // SPR-11025 + void beanMethodOverloadingWithInheritanceAndList() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(SubConfigWithList.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); @@ -159,13 +170,14 @@ public void beanMethodOverloadingWithInheritanceAndList() { * so it's referred to here as 'shadowing' to distinguish the difference. */ @Test - public void beanMethodShadowing() { + void beanMethodShadowing() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ShadowConfig.class); + assertThat(ctx.getBean(String.class)).isEqualTo("shadow"); } @Test - public void beanMethodThroughAopProxy() { + void beanMethodThroughAopProxy() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(Config.class); ctx.register(AnnotationAwareAspectJAutoProxyCreator.class); 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 e669752de69e..dbc346cb315f 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,13 +51,13 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class ClassPathBeanDefinitionScannerTests { +class ClassPathBeanDefinitionScannerTests { private static final String BASE_PACKAGE = "example.scannable"; @Test - public void testSimpleScanWithDefaultFiltersAndPostProcessors() { + void testSimpleScanWithDefaultFiltersAndPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); @@ -83,7 +83,7 @@ public void testSimpleScanWithDefaultFiltersAndPostProcessors() { } @Test - public void testSimpleScanWithDefaultFiltersAndPrimaryLazyBean() { + void testSimpleScanWithDefaultFiltersAndPrimaryLazyBean() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.scan(BASE_PACKAGE); @@ -105,7 +105,7 @@ public void testSimpleScanWithDefaultFiltersAndPrimaryLazyBean() { } @Test - public void testDoubleScan() { + void testDoubleScan() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); @@ -130,7 +130,7 @@ protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, } @Test - public void testWithIndex() { + void testWithIndex() { GenericApplicationContext context = new GenericApplicationContext(); context.setClassLoader(CandidateComponentsTestClassLoader.index( ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), @@ -149,7 +149,7 @@ public void testWithIndex() { } @Test - public void testDoubleScanWithIndex() { + void testDoubleScanWithIndex() { GenericApplicationContext context = new GenericApplicationContext(); context.setClassLoader(CandidateComponentsTestClassLoader.index( ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), @@ -177,7 +177,7 @@ protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, } @Test - public void testSimpleScanWithDefaultFiltersAndNoPostProcessors() { + void testSimpleScanWithDefaultFiltersAndNoPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); @@ -192,7 +192,7 @@ public void testSimpleScanWithDefaultFiltersAndNoPostProcessors() { } @Test - public void testSimpleScanWithDefaultFiltersAndOverridingBean() { + void testSimpleScanWithDefaultFiltersAndOverridingBean() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); @@ -203,7 +203,32 @@ public void testSimpleScanWithDefaultFiltersAndOverridingBean() { } @Test - public void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { + void testSimpleScanWithDefaultFiltersAndOverridingBeanNotAllowed() { + GenericApplicationContext context = new GenericApplicationContext(); + context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false); + context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + + assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE)) + .withMessageContaining("stubFooDao") + .withMessageContaining(StubFooDao.class.getName()); + } + + @Test + void testSimpleScanWithDefaultFiltersAndOverridingBeanAcceptedForSameBeanClass() { + GenericApplicationContext context = new GenericApplicationContext(); + context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false); + context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(StubFooDao.class)); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + + // should not fail! + scanner.scan(BASE_PACKAGE); + } + + @Test + void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); @@ -215,7 +240,7 @@ public void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { } @Test - public void testSimpleScanWithDefaultFiltersAndOverriddenEqualNamedBean() { + void testSimpleScanWithDefaultFiltersAndOverriddenEqualNamedBean() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("myNamedDao", new RootBeanDefinition(NamedStubDao.class)); int initialBeanCount = context.getBeanDefinitionCount(); @@ -233,7 +258,7 @@ public void testSimpleScanWithDefaultFiltersAndOverriddenEqualNamedBean() { } @Test - public void testSimpleScanWithDefaultFiltersAndOverriddenCompatibleNamedBean() { + void testSimpleScanWithDefaultFiltersAndOverriddenCompatibleNamedBean() { GenericApplicationContext context = new GenericApplicationContext(); RootBeanDefinition bd = new RootBeanDefinition(NamedStubDao.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -253,7 +278,7 @@ public void testSimpleScanWithDefaultFiltersAndOverriddenCompatibleNamedBean() { } @Test - public void testSimpleScanWithDefaultFiltersAndSameBeanTwice() { + void testSimpleScanWithDefaultFiltersAndSameBeanTwice() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); @@ -263,11 +288,12 @@ public void testSimpleScanWithDefaultFiltersAndSameBeanTwice() { } @Test - public void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { + void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); scanner.scan("org.springframework.context.annotation2"); + assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE)) .withMessageContaining("myNamedDao") .withMessageContaining(NamedStubDao.class.getName()) @@ -275,7 +301,7 @@ public void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { } @Test - public void testCustomIncludeFilterWithoutDefaultsButIncludingPostProcessors() { + void testCustomIncludeFilterWithoutDefaultsButIncludingPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); @@ -290,7 +316,7 @@ public void testCustomIncludeFilterWithoutDefaultsButIncludingPostProcessors() { } @Test - public void testCustomIncludeFilterWithoutDefaultsAndNoPostProcessors() { + void testCustomIncludeFilterWithoutDefaultsAndNoPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); @@ -310,7 +336,7 @@ public void testCustomIncludeFilterWithoutDefaultsAndNoPostProcessors() { } @Test - public void testCustomIncludeFilterAndDefaults() { + void testCustomIncludeFilterAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); @@ -330,7 +356,7 @@ public void testCustomIncludeFilterAndDefaults() { } @Test - public void testCustomAnnotationExcludeFilterAndDefaults() { + void testCustomAnnotationExcludeFilterAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); @@ -348,7 +374,7 @@ public void testCustomAnnotationExcludeFilterAndDefaults() { } @Test - public void testCustomAssignableTypeExcludeFilterAndDefaults() { + void testCustomAssignableTypeExcludeFilterAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); @@ -367,7 +393,7 @@ public void testCustomAssignableTypeExcludeFilterAndDefaults() { } @Test - public void testCustomAssignableTypeExcludeFilterAndDefaultsWithoutPostProcessors() { + void testCustomAssignableTypeExcludeFilterAndDefaultsWithoutPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.setIncludeAnnotationConfig(false); @@ -385,7 +411,7 @@ public void testCustomAssignableTypeExcludeFilterAndDefaultsWithoutPostProcessor } @Test - public void testMultipleCustomExcludeFiltersAndDefaults() { + void testMultipleCustomExcludeFiltersAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); @@ -405,7 +431,7 @@ public void testMultipleCustomExcludeFiltersAndDefaults() { } @Test - public void testCustomBeanNameGenerator() { + void testCustomBeanNameGenerator() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); @@ -425,7 +451,7 @@ public void testCustomBeanNameGenerator() { } @Test - public void testMultipleBasePackagesWithDefaultsOnly() { + void testMultipleBasePackagesWithDefaultsOnly() { GenericApplicationContext singlePackageContext = new GenericApplicationContext(); ClassPathBeanDefinitionScanner singlePackageScanner = new ClassPathBeanDefinitionScanner(singlePackageContext); GenericApplicationContext multiPackageContext = new GenericApplicationContext(); @@ -437,7 +463,7 @@ public void testMultipleBasePackagesWithDefaultsOnly() { } @Test - public void testMultipleScanCalls() { + void testMultipleScanCalls() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int initialBeanCount = context.getBeanDefinitionCount(); @@ -449,7 +475,7 @@ public void testMultipleScanCalls() { } @Test - public void testBeanAutowiredWithAnnotationConfigEnabled() { + void testBeanAutowiredWithAnnotationConfigEnabled() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("myBf", new RootBeanDefinition(StaticListableBeanFactory.class)); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); @@ -465,21 +491,18 @@ public void testBeanAutowiredWithAnnotationConfigEnabled() { assertThat(fooService.foo(123)).isEqualTo("bar"); assertThat(fooService.lookupFoo(123)).isEqualTo("bar"); assertThat(fooService.beanFactory).isSameAs(context.getDefaultListableBeanFactory()); - assertThat(fooService.listableBeanFactory).hasSize(2); - assertThat(fooService.listableBeanFactory.get(0)).isSameAs(context.getDefaultListableBeanFactory()); - assertThat(fooService.listableBeanFactory.get(1)).isSameAs(myBf); + assertThat(fooService.listableBeanFactory).containsExactly(context.getDefaultListableBeanFactory(), myBf); assertThat(fooService.resourceLoader).isSameAs(context); assertThat(fooService.resourcePatternResolver).isSameAs(context); assertThat(fooService.eventPublisher).isSameAs(context); assertThat(fooService.messageSource).isSameAs(ms); assertThat(fooService.context).isSameAs(context); - assertThat(fooService.configurableContext).hasSize(1); - assertThat(fooService.configurableContext[0]).isSameAs(context); + assertThat(fooService.configurableContext).containsExactly(context); assertThat(fooService.genericContext).isSameAs(context); } @Test - public void testBeanNotAutowiredWithAnnotationConfigDisabled() { + void testBeanNotAutowiredWithAnnotationConfigDisabled() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); @@ -498,7 +521,7 @@ public void testBeanNotAutowiredWithAnnotationConfigDisabled() { } @Test - public void testAutowireCandidatePatternMatches() { + void testAutowireCandidatePatternMatches() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(true); @@ -513,7 +536,7 @@ public void testAutowireCandidatePatternMatches() { } @Test - public void testAutowireCandidatePatternDoesNotMatch() { + void testAutowireCandidatePatternDoesNotMatch() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(true); 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 5e79ff768b16..0f540b8615c7 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,13 +37,13 @@ * @author Mark Pollack * @author Juergen Hoeller */ -public class ClassPathFactoryBeanDefinitionScannerTests { +class ClassPathFactoryBeanDefinitionScannerTests { private static final String BASE_PACKAGE = FactoryMethodComponent.class.getPackage().getName(); @Test - public void testSingletonScopedFactoryMethod() { + void testSingletonScopedFactoryMethod() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); 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 9c6ac57b4e87..f7880f4910dc 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 @@ -29,6 +29,8 @@ 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,6 +42,8 @@ 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; @@ -100,9 +104,16 @@ class ClassPathScanningCandidateComponentProviderTests { JakartaManagedBeanComponent.class ); - private static final Set> indexedJakartaComponents = Set.of( + private static final Set> scannedJavaxComponents = Set.of( + JavaxNamedComponent.class, + JavaxManagedBeanComponent.class + ); + + private static final Set> indexedComponents = Set.of( IndexedJakartaNamedComponent.class, - IndexedJakartaManagedBeanComponent.class + IndexedJakartaManagedBeanComponent.class, + IndexedJavaxNamedComponent.class, + IndexedJavaxManagedBeanComponent.class ); @@ -111,25 +122,28 @@ void defaultsWithScan() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); provider.setResourceLoader(new DefaultResourceLoader( CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); - testDefault(provider, TEST_BASE_PACKAGE, true, false); + testDefault(provider, TEST_BASE_PACKAGE, true, true, false); } @Test void defaultsWithIndex() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); - testDefault(provider, "example", true, true); + testDefault(provider, "example", true, true, true); } private void testDefault(ClassPathScanningCandidateComponentProvider provider, String basePackage, - boolean includeScannedJakartaComponents, boolean includeIndexedJakartaComponents) { + boolean includeScannedJakartaComponents, boolean includeScannedJavaxComponents, boolean includeIndexedComponents) { Set> expectedTypes = new HashSet<>(springComponents); if (includeScannedJakartaComponents) { expectedTypes.addAll(scannedJakartaComponents); } - if (includeIndexedJakartaComponents) { - expectedTypes.addAll(indexedJakartaComponents); + if (includeScannedJavaxComponents) { + expectedTypes.addAll(scannedJavaxComponents); + } + if (includeIndexedComponents) { + expectedTypes.addAll(indexedComponents); } Set candidates = provider.findCandidateComponents(basePackage); @@ -202,7 +216,7 @@ void customAnnotationTypeIncludeFilterWithIndex() { private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) { provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); - testDefault(provider, TEST_BASE_PACKAGE, false, false); + testDefault(provider, TEST_BASE_PACKAGE, false, false, false); } @Test @@ -295,7 +309,7 @@ private void testExclude(ClassPathScanningCandidateComponentProvider provider) { Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class, - BarComponent.class, JakartaManagedBeanComponent.class); + BarComponent.class, JakartaManagedBeanComponent.class, JavaxManagedBeanComponent.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 daf59a936bfe..b08c67573eb2 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import jakarta.annotation.PreDestroy; import jakarta.annotation.Resource; import jakarta.ejb.EJB; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.BeansException; @@ -46,15 +47,41 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link CommonAnnotationBeanPostProcessor} and + * {@link InitDestroyAnnotationBeanPostProcessor}. + * * @author Juergen Hoeller * @author Chris Beams + * @author Sam Brannen */ -public class CommonAnnotationBeanPostProcessorTests { +class CommonAnnotationBeanPostProcessorTests { + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + + @BeforeEach + void setup() { + bpp.setResourceFactory(bf); + bf.addBeanPostProcessor(bpp); + } @Test - public void testPostConstructAndPreDestroy() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + void processInjection() { + ResourceInjectionBean bean = new ResourceInjectionBean(); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + bpp.processInjection(bean); + + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + } + + @Test + void postConstructAndPreDestroy() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedInitDestroyBean.class)); AnnotatedInitDestroyBean bean = (AnnotatedInitDestroyBean) bf.getBean("annotatedBean"); @@ -64,10 +91,9 @@ public void testPostConstructAndPreDestroy() { } @Test - public void testPostConstructAndPreDestroyWithPostProcessor() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void postConstructAndPreDestroyWithPostProcessor() { bf.addBeanPostProcessor(new InitDestroyBeanPostProcessor()); - bf.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + bf.addBeanPostProcessor(bpp); bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedInitDestroyBean.class)); AnnotatedInitDestroyBean bean = (AnnotatedInitDestroyBean) bf.getBean("annotatedBean"); @@ -77,7 +103,7 @@ public void testPostConstructAndPreDestroyWithPostProcessor() { } @Test - public void testPostConstructAndPreDestroyWithApplicationContextAndPostProcessor() { + void postConstructAndPreDestroyWithApplicationContextAndPostProcessor() { GenericApplicationContext ctx = new GenericApplicationContext(); ctx.registerBeanDefinition("bpp1", new RootBeanDefinition(InitDestroyBeanPostProcessor.class)); ctx.registerBeanDefinition("bpp2", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); @@ -91,9 +117,7 @@ public void testPostConstructAndPreDestroyWithApplicationContextAndPostProcessor } @Test - public void testPostConstructAndPreDestroyWithLegacyAnnotations() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + void postConstructAndPreDestroyWithLegacyAnnotations() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LegacyAnnotatedInitDestroyBean.class)); LegacyAnnotatedInitDestroyBean bean = (LegacyAnnotatedInitDestroyBean) bf.getBean("annotatedBean"); @@ -103,12 +127,10 @@ public void testPostConstructAndPreDestroyWithLegacyAnnotations() { } @Test - public void testPostConstructAndPreDestroyWithManualConfiguration() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void postConstructAndPreDestroyWithManualConfiguration() { InitDestroyAnnotationBeanPostProcessor bpp = new InitDestroyAnnotationBeanPostProcessor(); bpp.setInitAnnotationType(PostConstruct.class); bpp.setDestroyAnnotationType(PreDestroy.class); - bf.addBeanPostProcessor(bpp); bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedInitDestroyBean.class)); AnnotatedInitDestroyBean bean = (AnnotatedInitDestroyBean) bf.getBean("annotatedBean"); @@ -118,9 +140,7 @@ public void testPostConstructAndPreDestroyWithManualConfiguration() { } @Test - public void testPostProcessorWithNullBean() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + void postProcessorWithNullBean() { RootBeanDefinition rbd = new RootBeanDefinition(NullFactory.class); rbd.setFactoryMethodName("create"); bf.registerBeanDefinition("bean", rbd); @@ -130,8 +150,7 @@ public void testPostProcessorWithNullBean() { } @Test - public void testSerialization() throws Exception { - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + void serialization() throws Exception { CommonAnnotationBeanPostProcessor bpp2 = SerializationTestUtils.serializeAndDeserialize(bpp); AnnotatedInitDestroyBean bean = new AnnotatedInitDestroyBean(); @@ -140,7 +159,7 @@ public void testSerialization() throws Exception { } @Test - public void testSerializationWithManualConfiguration() throws Exception { + void serializationWithManualConfiguration() throws Exception { InitDestroyAnnotationBeanPostProcessor bpp = new InitDestroyAnnotationBeanPostProcessor(); bpp.setInitAnnotationType(PostConstruct.class); bpp.setDestroyAnnotationType(PreDestroy.class); @@ -152,11 +171,7 @@ public void testSerializationWithManualConfiguration() throws Exception { } @Test - public void testResourceInjection() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setResourceFactory(bf); - bf.addBeanPostProcessor(bpp); + void resourceInjection() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ResourceInjectionBean.class)); TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); @@ -176,11 +191,7 @@ public void testResourceInjection() { } @Test - public void testResourceInjectionWithPrototypes() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setResourceFactory(bf); - bf.addBeanPostProcessor(bpp); + void resourceInjectionWithPrototypes() { RootBeanDefinition abd = new RootBeanDefinition(ResourceInjectionBean.class); abd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", abd); @@ -213,11 +224,7 @@ public void testResourceInjectionWithPrototypes() { } @Test - public void testResourceInjectionWithLegacyAnnotations() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setResourceFactory(bf); - bf.addBeanPostProcessor(bpp); + void resourceInjectionWithLegacyAnnotations() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LegacyResourceInjectionBean.class)); TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); @@ -237,9 +244,7 @@ public void testResourceInjectionWithLegacyAnnotations() { } @Test - public void testResourceInjectionWithResolvableDependencyType() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + void resourceInjectionWithResolvableDependencyType() { bpp.setBeanFactory(bf); bf.addBeanPostProcessor(bpp); RootBeanDefinition abd = new RootBeanDefinition(ExtendedResourceInjectionBean.class); @@ -273,11 +278,7 @@ public void testResourceInjectionWithResolvableDependencyType() { } @Test - public void testResourceInjectionWithDefaultMethod() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setBeanFactory(bf); - bf.addBeanPostProcessor(bpp); + void resourceInjectionWithDefaultMethod() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(DefaultMethodResourceInjectionBean.class)); TestBean tb2 = new TestBean(); bf.registerSingleton("testBean2", tb2); @@ -293,11 +294,7 @@ public void testResourceInjectionWithDefaultMethod() { } @Test - public void testResourceInjectionWithTwoProcessors() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setResourceFactory(bf); - bf.addBeanPostProcessor(bpp); + void resourceInjectionWithTwoProcessors() { CommonAnnotationBeanPostProcessor bpp2 = new CommonAnnotationBeanPostProcessor(); bpp2.setResourceFactory(bf); bf.addBeanPostProcessor(bpp2); @@ -318,9 +315,7 @@ public void testResourceInjectionWithTwoProcessors() { } @Test - public void testResourceInjectionFromJndi() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + void resourceInjectionFromJndi() { SimpleJndiBeanFactory resourceFactory = new SimpleJndiBeanFactory(); ExpectedLookupTemplate jndiTemplate = new ExpectedLookupTemplate(); TestBean tb = new TestBean(); @@ -329,7 +324,6 @@ public void testResourceInjectionFromJndi() { jndiTemplate.addObject("java:comp/env/testBean2", tb2); resourceFactory.setJndiTemplate(jndiTemplate); bpp.setResourceFactory(resourceFactory); - bf.addBeanPostProcessor(bpp); bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ResourceInjectionBean.class)); ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); @@ -343,9 +337,7 @@ public void testResourceInjectionFromJndi() { } @Test - public void testExtendedResourceInjection() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + void extendedResourceInjection() { bpp.setBeanFactory(bf); bf.addBeanPostProcessor(bpp); bf.registerResolvableDependency(BeanFactory.class, bf); @@ -396,9 +388,7 @@ public void testExtendedResourceInjection() { } @Test - public void testExtendedResourceInjectionWithOverriding() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + void extendedResourceInjectionWithOverriding() { bpp.setBeanFactory(bf); bf.addBeanPostProcessor(bpp); bf.registerResolvableDependency(BeanFactory.class, bf); @@ -453,9 +443,7 @@ public void testExtendedResourceInjectionWithOverriding() { } @Test - public void testExtendedEjbInjection() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + void extendedEjbInjection() { bpp.setBeanFactory(bf); bf.addBeanPostProcessor(bpp); bf.registerResolvableDependency(BeanFactory.class, bf); @@ -490,12 +478,7 @@ public void testExtendedEjbInjection() { } @Test - public void testLazyResolutionWithResourceField() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setBeanFactory(bf); - bf.addBeanPostProcessor(bpp); - + void lazyResolutionWithResourceField() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LazyResourceFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -508,12 +491,7 @@ public void testLazyResolutionWithResourceField() { } @Test - public void testLazyResolutionWithResourceMethod() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setBeanFactory(bf); - bf.addBeanPostProcessor(bpp); - + void lazyResolutionWithResourceMethod() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LazyResourceMethodInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -526,12 +504,7 @@ public void testLazyResolutionWithResourceMethod() { } @Test - public void testLazyResolutionWithCglibProxy() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); - bpp.setBeanFactory(bf); - bf.addBeanPostProcessor(bpp); - + void lazyResolutionWithCglibProxy() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LazyResourceCglibInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); @@ -544,10 +517,8 @@ public void testLazyResolutionWithCglibProxy() { } @Test - public void testLazyResolutionWithFallbackTypeMatch() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void lazyResolutionWithFallbackTypeMatch() { bf.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver()); - CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); bpp.setBeanFactory(bf); bf.addBeanPostProcessor(bpp); 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 new file mode 100644 index 000000000000..7f4e591bc4f4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationCode; +import org.springframework.context.testfixture.context.annotation.PackagePrivateFieldResourceSample; +import org.springframework.context.testfixture.context.annotation.PackagePrivateMethodResourceSample; +import org.springframework.context.testfixture.context.annotation.PrivateFieldResourceSample; +import org.springframework.context.testfixture.context.annotation.PrivateMethodResourceSample; +import org.springframework.context.testfixture.context.annotation.PrivateMethodResourceWithCustomNameSample; +import org.springframework.context.testfixture.context.annotation.PublicMethodResourceSample; +import org.springframework.context.testfixture.context.annotation.subpkg.PackagePrivateFieldResourceFromParentSample; +import org.springframework.context.testfixture.context.annotation.subpkg.PackagePrivateMethodResourceFromParentSample; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.SourceFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for AOT contributions of {@link CommonAnnotationBeanPostProcessor}. + * + * @author Stephane Nicoll + */ +class CommonAnnotationBeanRegistrationAotContributionTests { + + private final TestGenerationContext generationContext; + + private final MockBeanRegistrationCode beanRegistrationCode; + + private final DefaultListableBeanFactory beanFactory; + + private final CommonAnnotationBeanPostProcessor beanPostProcessor; + + CommonAnnotationBeanRegistrationAotContributionTests() { + this.generationContext = new TestGenerationContext(); + this.beanRegistrationCode = new MockBeanRegistrationCode(this.generationContext); + this.beanFactory = new DefaultListableBeanFactory(); + this.beanPostProcessor = new CommonAnnotationBeanPostProcessor(); + this.beanPostProcessor.setBeanFactory(this.beanFactory); + } + + @Test + void contributeWhenPrivateFieldInjectionInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateFieldResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PrivateFieldResourceSample.class, "one")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateFieldResourceSample instance = new PrivateFieldResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PrivateFieldResourceSample.class)) + .contains("resolveAndSet("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateFieldInjectionInjectsUsingFieldAssignement() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateFieldResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PackagePrivateFieldResourceSample.class, "one")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateFieldResourceSample instance = new PackagePrivateFieldResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateFieldResourceSample.class)) + .contains("instance.one ="); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateFieldInjectionOnParentClassInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateFieldResourceFromParentSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PackagePrivateFieldResourceSample.class, "one")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateFieldResourceFromParentSample instance = new PackagePrivateFieldResourceFromParentSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateFieldResourceFromParentSample.class)) + .contains("resolveAndSet"); + }); + } + + @Test + void contributeWhenPrivateMethodInjectionInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateMethodResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PrivateMethodResourceSample.class, "setOne").invoke()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateMethodResourceSample instance = new PrivateMethodResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PrivateMethodResourceSample.class)) + .contains("resolveAndSet("); + }); + } + + @Test + void contributeWhenPrivateMethodInjectionWithCustomNameInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateMethodResourceWithCustomNameSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PrivateMethodResourceWithCustomNameSample.class, "setText").invoke()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateMethodResourceWithCustomNameSample instance = new PrivateMethodResourceWithCustomNameSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("text").isEqualTo("1"); + assertThat(getSourceFile(compiled, PrivateMethodResourceWithCustomNameSample.class)) + .contains("resolveAndSet("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateMethodInjectionInjectsUsingMethodInvocation() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateMethodResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PackagePrivateMethodResourceSample.class, "setOne").introspect()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateMethodResourceSample instance = new PackagePrivateMethodResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateMethodResourceSample.class)) + .contains("instance.setOne("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateMethodInjectionOnParentClassInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateMethodResourceFromParentSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PackagePrivateMethodResourceSample.class, "setOne")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateMethodResourceFromParentSample instance = new PackagePrivateMethodResourceFromParentSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateMethodResourceFromParentSample.class)) + .contains("resolveAndSet("); + }); + } + + @Test + void contributeWhenMethodInjectionHasMatchingPropertyValue() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(PublicMethodResourceSample.class); + beanDefinition.getPropertyValues().addPropertyValue("one", "from-property"); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + BeanRegistrationAotContribution contribution = this.beanPostProcessor + .processAheadOfTime(RegisteredBean.of(this.beanFactory, "test")); + assertThat(contribution).isNull(); + } + + private RegisteredBean getAndApplyContribution(Class beanClass) { + RegisteredBean registeredBean = registerBean(beanClass); + BeanRegistrationAotContribution contribution = this.beanPostProcessor + .processAheadOfTime(registeredBean); + assertThat(contribution).isNotNull(); + contribution.applyTo(this.generationContext, this.beanRegistrationCode); + return registeredBean; + } + + private RegisteredBean registerBean(Class beanClass) { + String beanName = "testBean"; + this.beanFactory.registerBeanDefinition(beanName, + new RootBeanDefinition(beanClass)); + return RegisteredBean.of(this.beanFactory, beanName); + } + + private static SourceFile getSourceFile(Compiled compiled, Class sample) { + return compiled.getSourceFileFromPackage(sample.getPackageName()); + } + + + @SuppressWarnings("unchecked") + private void compile(RegisteredBean registeredBean, + BiConsumer, Compiled> result) { + Class target = registeredBean.getBeanClass(); + MethodReference methodReference = this.beanRegistrationCode.getInstancePostProcessors().get(0); + this.beanRegistrationCode.getTypeBuilder().set(type -> { + CodeBlock methodInvocation = methodReference.toInvokeCodeBlock( + MethodReference.ArgumentCodeGenerator.of(RegisteredBean.class, "registeredBean") + .and(target, "instance"), this.beanRegistrationCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get( + BiFunction.class, RegisteredBean.class, target, target)); + type.addMethod(MethodSpec.methodBuilder("apply") + .addModifiers(Modifier.PUBLIC) + .addParameter(RegisteredBean.class, "registeredBean") + .addParameter(target, "instance") + .returns(target) + .addStatement("return $L", methodInvocation) + .build()); + + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).printFiles(System.out) + .compile(compiled -> result.accept(compiled.getInstance(BiFunction.class), compiled)); + } + +} 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 015075456768..0603f4d6193c 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,11 @@ package org.springframework.context.annotation; -import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.HashSet; +import java.util.Set; import example.scannable.CustomComponent; import example.scannable.CustomStereotype; @@ -44,6 +43,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.context.ApplicationContext; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ComponentScan.Filter; @@ -55,7 +55,6 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; -import org.springframework.core.env.Profiles; import org.springframework.core.io.ResourceLoader; import org.springframework.core.testfixture.io.SerializationTestUtils; import org.springframework.core.type.classreading.MetadataReader; @@ -74,139 +73,179 @@ * @since 3.1 */ @SuppressWarnings("resource") -public class ComponentScanAnnotationIntegrationTests { +class ComponentScanAnnotationIntegrationTests { @Test - public void controlScan() { + void controlScan() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.scan(example.scannable.PackageMarker.class.getPackage().getName()); ctx.refresh(); - assertThat(ctx.containsBean("fooServiceImpl")).as( - "control scan for example.scannable package failed to register FooServiceImpl bean").isTrue(); + + assertContextContainsBean(ctx, "fooServiceImpl"); } @Test - public void viaContextRegistration() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanAnnotatedConfig.class); - ctx.refresh(); + void viaContextRegistration() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanAnnotatedConfig.class); ctx.getBean(ComponentScanAnnotatedConfig.class); ctx.getBean(TestBean.class); - assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig")).as("config class bean not found") - .isTrue(); + + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig")).as("config class bean not found").isTrue(); assertThat(ctx.containsBean("fooServiceImpl")).as("@ComponentScan annotated @Configuration class registered directly against " + - "AnnotationConfigApplicationContext did not trigger component scanning as expected") - .isTrue(); + "AnnotationConfigApplicationContext did not trigger component scanning as expected").isTrue(); } @Test - public void viaContextRegistration_WithValueAttribute() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanAnnotatedConfig_WithValueAttribute.class); - ctx.refresh(); + void viaContextRegistration_WithValueAttribute() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanAnnotatedConfig_WithValueAttribute.class); ctx.getBean(ComponentScanAnnotatedConfig_WithValueAttribute.class); ctx.getBean(TestBean.class); - assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig_WithValueAttribute")).as("config class bean not found") - .isTrue(); + + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig_WithValueAttribute")).as("config class bean not found").isTrue(); assertThat(ctx.containsBean("fooServiceImpl")).as("@ComponentScan annotated @Configuration class registered directly against " + - "AnnotationConfigApplicationContext did not trigger component scanning as expected") - .isTrue(); + "AnnotationConfigApplicationContext did not trigger component scanning as expected").isTrue(); } @Test - public void viaContextRegistration_FromPackageOfConfigClass() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanAnnotatedConfigWithImplicitBasePackage.class); - ctx.refresh(); + void viaContextRegistration_FromPackageOfConfigClass() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanAnnotatedConfigWithImplicitBasePackage.class); ctx.getBean(ComponentScanAnnotatedConfigWithImplicitBasePackage.class); - assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfigWithImplicitBasePackage")).as("config class bean not found") - .isTrue(); + + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfigWithImplicitBasePackage")).as("config class bean not found").isTrue(); assertThat(ctx.containsBean("scannedComponent")).as("@ComponentScan annotated @Configuration class registered directly against " + - "AnnotationConfigApplicationContext did not trigger component scanning as expected") - .isTrue(); - assertThat(ctx.getBean(ConfigurableComponent.class).isFlag()).as("@Bean method overrides scanned class") - .isTrue(); + "AnnotationConfigApplicationContext did not trigger component scanning as expected").isTrue(); + assertThat(ctx.getBean(ConfigurableComponent.class).isFlag()).as("@Bean method overrides scanned class").isTrue(); } @Test - public void viaContextRegistration_WithComposedAnnotation() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComposedAnnotationConfig.class); - ctx.refresh(); + void viaContextRegistration_WithComposedAnnotation() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComposedAnnotationConfig.class); ctx.getBean(ComposedAnnotationConfig.class); ctx.getBean(SimpleComponent.class); ctx.getBean(ClassWithNestedComponents.NestedComponent.class); ctx.getBean(ClassWithNestedComponents.OtherNestedComponent.class); - assertThat(ctx.containsBeanDefinition("componentScanAnnotationIntegrationTests.ComposedAnnotationConfig")).as("config class bean not found") - .isTrue(); + + assertThat(ctx.containsBeanDefinition("componentScanAnnotationIntegrationTests.ComposedAnnotationConfig")).as("config class bean not found").isTrue(); assertThat(ctx.containsBean("simpleComponent")).as("@ComponentScan annotated @Configuration class registered directly against " + - "AnnotationConfigApplicationContext did not trigger component scanning as expected") - .isTrue(); + "AnnotationConfigApplicationContext did not trigger component scanning as expected").isTrue(); + } + + @Test + void multipleComposedComponentScanAnnotations() { // gh-30941 + ApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class); + ctx.getBean(MultipleComposedAnnotationsConfig.class); + + assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.MultipleComposedAnnotationsConfig"); + assertContextContainsBean(ctx, "simpleComponent"); + assertContextContainsBean(ctx, "barComponent"); + } + + @Test + void localAnnotationOverridesMultipleMetaAnnotations() { // gh-31704 + ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalAnnotationOverridesMultipleMetaAnnotationsConfig.class); + + assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalAnnotationOverridesMultipleMetaAnnotationsConfig"); + assertContextContainsBean(ctx, "barComponent"); + assertContextDoesNotContainBean(ctx, "simpleComponent"); + assertContextDoesNotContainBean(ctx, "configurableComponent"); + } + + @Test + void localAnnotationOverridesMultipleComposedAnnotations() { // gh-31704 + ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalAnnotationOverridesMultipleComposedAnnotationsConfig.class); + + assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalAnnotationOverridesMultipleComposedAnnotationsConfig"); + assertContextContainsBean(ctx, "barComponent"); + assertContextDoesNotContainBean(ctx, "simpleComponent"); + assertContextDoesNotContainBean(ctx, "configurableComponent"); + } + + @Test + void localRepeatedAnnotationsOverrideComposedAnnotations() { // gh-31704 + ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig.class); + + assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig"); + assertContextContainsBean(ctx, "barComponent"); + assertContextContainsBean(ctx, "configurableComponent"); + assertContextDoesNotContainBean(ctx, "simpleComponent"); } @Test - public void viaBeanRegistration() { + void localRepeatedAnnotationsInContainerOverrideComposedAnnotations() { // gh-31704 + ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig.class); + + assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig"); + assertContextContainsBean(ctx, "barComponent"); + assertContextContainsBean(ctx, "configurableComponent"); + assertContextDoesNotContainBean(ctx, "simpleComponent"); + } + + @Test + void viaBeanRegistration() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("componentScanAnnotatedConfig", genericBeanDefinition(ComponentScanAnnotatedConfig.class).getBeanDefinition()); bf.registerBeanDefinition("configurationClassPostProcessor", genericBeanDefinition(ConfigurationClassPostProcessor.class).getBeanDefinition()); + GenericApplicationContext ctx = new GenericApplicationContext(bf); ctx.refresh(); ctx.getBean(ComponentScanAnnotatedConfig.class); ctx.getBean(TestBean.class); - assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig")).as("config class bean not found") - .isTrue(); + + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig")).as("config class bean not found").isTrue(); assertThat(ctx.containsBean("fooServiceImpl")).as("@ComponentScan annotated @Configuration class registered as bean " + - "definition did not trigger component scanning as expected") - .isTrue(); + "definition did not trigger component scanning as expected").isTrue(); } @Test - public void withCustomBeanNameGenerator() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanWithBeanNameGenerator.class); - ctx.refresh(); - assertThat(ctx.containsBean("custom_fooServiceImpl")).isTrue(); - assertThat(ctx.containsBean("fooServiceImpl")).isFalse(); + void withCustomBeanNameGenerator() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithBeanNameGenerator.class); + assertContextContainsBean(ctx, "custom_fooServiceImpl"); + assertContextDoesNotContainBean(ctx, "fooServiceImpl"); } @Test - public void withScopeResolver() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithScopeResolver.class); + void withScopeResolver() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithScopeResolver.class); + // custom scope annotation makes the bean prototype scoped. subsequent calls // to getBean should return distinct instances. assertThat(ctx.getBean(CustomScopeAnnotationBean.class)).isNotSameAs(ctx.getBean(CustomScopeAnnotationBean.class)); - assertThat(ctx.containsBean("scannedComponent")).isFalse(); + assertContextDoesNotContainBean(ctx, "scannedComponent"); } @Test - public void multiComponentScan() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MultiComponentScan.class); + void multiComponentScan() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(MultiComponentScan.class); + assertThat(ctx.getBean(CustomScopeAnnotationBean.class)).isNotSameAs(ctx.getBean(CustomScopeAnnotationBean.class)); - assertThat(ctx.containsBean("scannedComponent")).isTrue(); + assertContextContainsBean(ctx, "scannedComponent"); } @Test - public void withCustomTypeFilter() { + void withCustomTypeFilter() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithCustomTypeFilter.class); - assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("componentScanParserTests.KustomAnnotationAutowiredBean")).isFalse(); + + assertThat(ctx.getBeanFactory().containsSingleton("componentScanParserTests.KustomAnnotationAutowiredBean")).isFalse(); KustomAnnotationAutowiredBean testBean = ctx.getBean("componentScanParserTests.KustomAnnotationAutowiredBean", KustomAnnotationAutowiredBean.class); assertThat(testBean.getDependency()).isNotNull(); } @Test - public void withAwareTypeFilter() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithAwareTypeFilter.class); - assertThat(ctx.getEnvironment().acceptsProfiles(Profiles.of("the-filter-ran"))).isTrue(); + void withAwareTypeFilter() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithAwareTypeFilter.class); + + assertThat(ctx.getEnvironment().matchesProfiles("the-filter-ran")).isTrue(); } @Test - public void withScopedProxy() throws IOException, ClassNotFoundException { + void withScopedProxy() throws Exception { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ComponentScanWithScopedProxy.class); ctx.getBeanFactory().registerScope("myScope", new SimpleMapScope()); ctx.refresh(); + // should cast to the interface FooService bean = (FooService) ctx.getBean("scopedProxyTestBean"); // should be dynamic proxy @@ -219,11 +258,12 @@ public void withScopedProxy() throws IOException, ClassNotFoundException { } @Test - public void withScopedProxyThroughRegex() throws IOException, ClassNotFoundException { + void withScopedProxyThroughRegex() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ComponentScanWithScopedProxyThroughRegex.class); ctx.getBeanFactory().registerScope("myScope", new SimpleMapScope()); ctx.refresh(); + // should cast to the interface FooService bean = (FooService) ctx.getBean("scopedProxyTestBean"); // should be dynamic proxy @@ -231,11 +271,12 @@ public void withScopedProxyThroughRegex() throws IOException, ClassNotFoundExcep } @Test - public void withScopedProxyThroughAspectJPattern() throws IOException, ClassNotFoundException { + void withScopedProxyThroughAspectJPattern() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ComponentScanWithScopedProxyThroughAspectJPattern.class); ctx.getBeanFactory().registerScope("myScope", new SimpleMapScope()); ctx.refresh(); + // should cast to the interface FooService bean = (FooService) ctx.getBean("scopedProxyTestBean"); // should be dynamic proxy @@ -243,29 +284,53 @@ public void withScopedProxyThroughAspectJPattern() throws IOException, ClassNotF } @Test - public void withMultipleAnnotationIncludeFilters1() throws IOException, ClassNotFoundException { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters1.class); - ctx.refresh(); + void withMultipleAnnotationIncludeFilters1() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithMultipleAnnotationIncludeFilters1.class); + ctx.getBean(DefaultNamedComponent.class); // @CustomStereotype-annotated ctx.getBean(MessageBean.class); // @CustomComponent-annotated } @Test - public void withMultipleAnnotationIncludeFilters2() throws IOException, ClassNotFoundException { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters2.class); - ctx.refresh(); + void withMultipleAnnotationIncludeFilters2() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithMultipleAnnotationIncludeFilters2.class); + ctx.getBean(DefaultNamedComponent.class); // @CustomStereotype-annotated ctx.getBean(MessageBean.class); // @CustomComponent-annotated } @Test - public void withBasePackagesAndValueAlias() { + void withBeanMethodOverride() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters3.class); + ctx.refresh(); + + assertThat(ctx.getBean(DefaultNamedComponent.class).toString()).isEqualTo("overridden"); + } + + @Test + void withBeanMethodOverrideAndGeneralOverridingDisabled() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanWithBasePackagesAndValueAlias.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters3.class); ctx.refresh(); - assertThat(ctx.containsBean("fooServiceImpl")).isTrue(); + + assertThat(ctx.getBean(DefaultNamedComponent.class).toString()).isEqualTo("overridden"); + } + + @Test + void withBasePackagesAndValueAlias() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithBasePackagesAndValueAlias.class); + + assertContextContainsBean(ctx, "fooServiceImpl"); + } + + + private static void assertContextContainsBean(ApplicationContext ctx, String beanName) { + assertThat(ctx.containsBean(beanName)).as("context should contain bean " + beanName).isTrue(); + } + private static void assertContextDoesNotContainBean(ApplicationContext ctx, String beanName) { + assertThat(ctx.containsBean(beanName)).as("context should not contain bean " + beanName).isFalse(); } @@ -273,17 +338,73 @@ public void withBasePackagesAndValueAlias() { @ComponentScan @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - public @interface ComposedConfiguration { + @interface ComposedConfiguration { @AliasFor(annotation = ComponentScan.class) String[] basePackages() default {}; } + @Configuration + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface ComposedConfiguration2 { + + @AliasFor(annotation = ComponentScan.class) + String[] basePackages() default {}; + } + + @Configuration + @ComponentScan("org.springframework.context.annotation.componentscan.simple") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface MetaConfiguration1 { + } + + @Configuration + @ComponentScan("example.scannable_implicitbasepackage") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface MetaConfiguration2 { + } + + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") + static class ComposedAnnotationConfig { + } + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") - public static class ComposedAnnotationConfig { + @ComposedConfiguration2(basePackages = "example.scannable.sub") + static class MultipleComposedAnnotationsConfig { } - public static class AwareTypeFilter implements TypeFilter, EnvironmentAware, + @MetaConfiguration1 + @MetaConfiguration2 + @ComponentScan("example.scannable.sub") + static class LocalAnnotationOverridesMultipleMetaAnnotationsConfig { + } + + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") + @ComposedConfiguration2(basePackages = "example.scannable_implicitbasepackage") + @ComponentScan("example.scannable.sub") + static class LocalAnnotationOverridesMultipleComposedAnnotationsConfig { + } + + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") + @ComponentScan("example.scannable_implicitbasepackage") + @ComponentScan("example.scannable.sub") + static class LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig { + } + + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") + @ComponentScans({ + @ComponentScan("example.scannable_implicitbasepackage"), + @ComponentScan("example.scannable.sub") + }) + static class LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig { + } + + + static class AwareTypeFilter implements TypeFilter, EnvironmentAware, ResourceLoaderAware, BeanClassLoaderAware, BeanFactoryAware { private BeanFactory beanFactory; @@ -320,10 +441,8 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada assertThat(this.environment).isNotNull(); return false; } - } - } @@ -347,11 +466,6 @@ public TestBean testBean() { } } -@Configuration -@ComponentScan -class ComponentScanWithNoPackagesConfig { -} - @Configuration @ComponentScan(basePackages = "example.scannable", nameGenerator = MyBeanNameGenerator.class) class ComponentScanWithBeanNameGenerator { @@ -397,9 +511,7 @@ class ComponentScanWithCustomTypeFilter { @SuppressWarnings({ "rawtypes", "serial", "unchecked" }) public static CustomAutowireConfigurer customAutowireConfigurer() { CustomAutowireConfigurer cac = new CustomAutowireConfigurer(); - cac.setCustomQualifierTypes(new HashSet() {{ - add(ComponentScanParserTests.CustomAnnotation.class); - }}); + cac.setCustomQualifierTypes(Set.of(ComponentScanParserTests.CustomAnnotation.class)); return cac; } @@ -454,11 +566,27 @@ class ComponentScanWithMultipleAnnotationIncludeFilters1 {} ) class ComponentScanWithMultipleAnnotationIncludeFilters2 {} +@Configuration +@ComponentScan(basePackages = "example.scannable", + useDefaultFilters = false, + includeFilters = @Filter({CustomStereotype.class, CustomComponent.class}) +) +class ComponentScanWithMultipleAnnotationIncludeFilters3 { + + @Bean + public DefaultNamedComponent thoreau() { + return new DefaultNamedComponent() { + @Override + public String toString() { + return "overridden"; + } + }; + } +} + @Configuration @ComponentScan( value = "example.scannable", basePackages = "example.scannable", basePackageClasses = example.scannable.PackageMarker.class) class ComponentScanWithBasePackagesAndValueAlias {} - - 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 3853a6fc1f92..458608eed41b 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-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; @@ -23,44 +24,46 @@ import org.springframework.core.type.filter.TypeFilter; /** - * Unit tests for the @ComponentScan annotation. + * Tests for the {@code @ComponentScan} annotation. * * @author Chris Beams * @since 3.1 * @see ComponentScanAnnotationIntegrationTests */ -public class ComponentScanAnnotationTests { +@Disabled("Compilation of this test class is sufficient to serve its purpose") +class ComponentScanAnnotationTests { @Test - public void noop() { - // no-op; the @ComponentScan-annotated MyConfig class below simply exercises + void noop() { + // no-op; the @ComponentScan-annotated classes below simply exercise // available attributes of the annotation. } -} + @interface MyAnnotation { + } -@interface MyAnnotation { -} + @Configuration + @ComponentScan( + basePackageClasses = TestBean.class, + nameGenerator = DefaultBeanNameGenerator.class, + scopedProxy = ScopedProxyMode.NO, + scopeResolver = AnnotationScopeMetadataResolver.class, + resourcePattern = "**/*custom.class", + useDefaultFilters = false, + includeFilters = { + @Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class) + }, + excludeFilters = { + @Filter(type = FilterType.CUSTOM, value = TypeFilter.class) + }, + lazyInit = true + ) + static class MyConfig { + } -@Configuration -@ComponentScan( - basePackageClasses = TestBean.class, - nameGenerator = DefaultBeanNameGenerator.class, - scopedProxy = ScopedProxyMode.NO, - scopeResolver = AnnotationScopeMetadataResolver.class, - resourcePattern = "**/*custom.class", - useDefaultFilters = false, - includeFilters = { - @Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class) - }, - excludeFilters = { - @Filter(type = FilterType.CUSTOM, value = TypeFilter.class) - }, - lazyInit = true -) -class MyConfig { -} + @ComponentScan(basePackageClasses = example.scannable.NamedComponent.class) + static class SimpleConfig { + } -@ComponentScan(basePackageClasses = example.scannable.NamedComponent.class) -class SimpleConfig { } + 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 cbd1b5ca4f55..84deb54af237 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ * @author Mark Fisher * @author Chris Beams */ -public class ComponentScanParserBeanDefinitionDefaultsTests { +class ComponentScanParserBeanDefinitionDefaultsTests { private static final String TEST_BEAN_NAME = "componentScanParserBeanDefinitionDefaultsTests.DefaultsTestBean"; @@ -38,12 +38,12 @@ public class ComponentScanParserBeanDefinitionDefaultsTests { @BeforeEach - public void setUp() { + void setUp() { DefaultsTestBean.INIT_COUNT = 0; } @Test - public void testDefaultLazyInit() { + void testDefaultLazyInit() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -54,7 +54,7 @@ public void testDefaultLazyInit() { } @Test - public void testLazyInitTrue() { + void testLazyInitTrue() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultLazyInitTrueTests.xml"); @@ -67,7 +67,7 @@ public void testLazyInitTrue() { } @Test - public void testLazyInitFalse() { + void testLazyInitFalse() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultLazyInitFalseTests.xml"); @@ -78,7 +78,7 @@ public void testLazyInitFalse() { } @Test - public void testDefaultAutowire() { + void testDefaultAutowire() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -90,7 +90,7 @@ public void testDefaultAutowire() { } @Test - public void testAutowireNo() { + void testAutowireNo() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireNoTests.xml"); @@ -102,7 +102,7 @@ public void testAutowireNo() { } @Test - public void testAutowireConstructor() { + void testAutowireConstructor() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireConstructorTests.xml"); @@ -115,7 +115,7 @@ public void testAutowireConstructor() { } @Test - public void testAutowireByType() { + void testAutowireByType() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireByTypeTests.xml"); @@ -124,7 +124,7 @@ public void testAutowireByType() { } @Test - public void testAutowireByName() { + void testAutowireByName() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireByNameTests.xml"); @@ -137,7 +137,7 @@ public void testAutowireByName() { } @Test - public void testDefaultDependencyCheck() { + void testDefaultDependencyCheck() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -149,7 +149,7 @@ public void testDefaultDependencyCheck() { } @Test - public void testDefaultInitAndDestroyMethodsNotDefined() { + void testDefaultInitAndDestroyMethodsNotDefined() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -161,7 +161,7 @@ public void testDefaultInitAndDestroyMethodsNotDefined() { } @Test - public void testDefaultInitAndDestroyMethodsDefined() { + void testDefaultInitAndDestroyMethodsDefined() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultInitAndDestroyMethodsTests.xml"); @@ -173,7 +173,7 @@ public void testDefaultInitAndDestroyMethodsDefined() { } @Test - public void testDefaultNonExistingInitAndDestroyMethodsDefined() { + void testDefaultNonExistingInitAndDestroyMethodsDefined() { 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 e498e9470350..39603286200c 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-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,10 +34,10 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class ComponentScanParserScopedProxyTests { +class ComponentScanParserScopedProxyTests { @Test - public void testDefaultScopedProxy() { + void testDefaultScopedProxy() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyDefaultTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -49,7 +49,7 @@ public void testDefaultScopedProxy() { } @Test - public void testNoScopedProxy() { + void testNoScopedProxy() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyNoTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -61,7 +61,7 @@ public void testNoScopedProxy() { } @Test - public void testInterfacesScopedProxy() throws Exception { + void testInterfacesScopedProxy() throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyInterfacesTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -79,7 +79,7 @@ public void testInterfacesScopedProxy() throws Exception { } @Test - public void testTargetClassScopedProxy() throws Exception { + void testTargetClassScopedProxy() throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyTargetClassTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -97,7 +97,7 @@ public void testTargetClassScopedProxy() throws Exception { @Test @SuppressWarnings("resource") - public void testInvalidConfigScopedProxy() throws Exception { + public void testInvalidConfigScopedProxy() { 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 9bacf0846633..22e9fd690b52 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ * @author Chris Beams * @author Sam Brannen */ -public class ComponentScanParserTests { +class ComponentScanParserTests { private ClassPathXmlApplicationContext loadContext(String path) { return new ClassPathXmlApplicationContext(path, getClass()); @@ -49,7 +49,7 @@ private ClassPathXmlApplicationContext loadContext(String path) { @Test - public void aspectjTypeFilter() { + void aspectjTypeFilter() { ClassPathXmlApplicationContext context = loadContext("aspectjTypeFilterTests.xml"); assertThat(context.containsBean("fooServiceImpl")).isTrue(); assertThat(context.containsBean("stubFooDao")).isTrue(); @@ -58,7 +58,7 @@ public void aspectjTypeFilter() { } @Test - public void aspectjTypeFilterWithPlaceholders() { + void aspectjTypeFilterWithPlaceholders() { System.setProperty("basePackage", "example.scannable, test"); System.setProperty("scanInclude", "example.scannable.FooService+"); System.setProperty("scanExclude", "example..Scoped*Test*"); @@ -77,21 +77,21 @@ public void aspectjTypeFilterWithPlaceholders() { } @Test - public void nonMatchingResourcePattern() { + void nonMatchingResourcePattern() { ClassPathXmlApplicationContext context = loadContext("nonMatchingResourcePatternTests.xml"); assertThat(context.containsBean("fooServiceImpl")).isFalse(); context.close(); } @Test - public void matchingResourcePattern() { + void matchingResourcePattern() { ClassPathXmlApplicationContext context = loadContext("matchingResourcePatternTests.xml"); assertThat(context.containsBean("fooServiceImpl")).isTrue(); context.close(); } @Test - public void componentScanWithAutowiredQualifier() { + void componentScanWithAutowiredQualifier() { ClassPathXmlApplicationContext context = loadContext("componentScanWithAutowiredQualifierTests.xml"); AutowiredQualifierFooService fooService = (AutowiredQualifierFooService) context.getBean("fooService"); assertThat(fooService.isInitCalled()).isTrue(); @@ -100,7 +100,7 @@ public void componentScanWithAutowiredQualifier() { } @Test - public void customAnnotationUsedForBothComponentScanAndQualifier() { + void customAnnotationUsedForBothComponentScanAndQualifier() { ClassPathXmlApplicationContext context = loadContext("customAnnotationUsedForBothComponentScanAndQualifierTests.xml"); KustomAnnotationAutowiredBean testBean = (KustomAnnotationAutowiredBean) context.getBean("testBean"); assertThat(testBean.getDependency()).isNotNull(); @@ -108,7 +108,7 @@ public void customAnnotationUsedForBothComponentScanAndQualifier() { } @Test - public void customTypeFilter() { + void customTypeFilter() { ClassPathXmlApplicationContext context = loadContext("customTypeFilterTests.xml"); KustomAnnotationAutowiredBean testBean = (KustomAnnotationAutowiredBean) context.getBean("testBean"); assertThat(testBean.getDependency()).isNotNull(); @@ -116,7 +116,7 @@ public void customTypeFilter() { } @Test - public void componentScanRespectsProfileAnnotation() { + void componentScanRespectsProfileAnnotation() { String xmlLocation = "org/springframework/context/annotation/componentScanRespectsProfileAnnotationTests.xml"; { // should exclude the profile-annotated bean if active profiles remains unset GenericXmlApplicationContext context = new GenericXmlApplicationContext(); 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 a90213764d41..90c1185048fa 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,7 +88,7 @@ static class AutowiredConfigWithBFPPAsStaticMethod { @Autowired TestBean autowiredTestBean; @Bean - public static final BeanFactoryPostProcessor bfpp() { + public static BeanFactoryPostProcessor bfpp() { return beanFactory -> { // no-op }; 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 468b090474fd..91876dbdf6a3 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,16 +44,16 @@ void verifyEquals() throws Exception { ConfigurationClass configurationClass2 = newConfigurationClass(Config1.class); ConfigurationClass configurationClass3 = newConfigurationClass(Config2.class); - assertThat(configurationClass1.equals(null)).isFalse(); + assertThat(configurationClass1).isNotEqualTo(null); assertThat(configurationClass1).isNotSameAs(configurationClass2); - assertThat(configurationClass1.equals(configurationClass1)).isTrue(); - assertThat(configurationClass2.equals(configurationClass2)).isTrue(); - assertThat(configurationClass1.equals(configurationClass2)).isTrue(); - assertThat(configurationClass2.equals(configurationClass1)).isTrue(); + assertThat(configurationClass1).isEqualTo(configurationClass1); + assertThat(configurationClass2).isEqualTo(configurationClass2); + assertThat(configurationClass1).isEqualTo(configurationClass2); + assertThat(configurationClass2).isEqualTo(configurationClass1); - assertThat(configurationClass1.equals(configurationClass3)).isFalse(); - assertThat(configurationClass3.equals(configurationClass2)).isFalse(); + assertThat(configurationClass1).isNotEqualTo(configurationClass3); + assertThat(configurationClass3).isNotEqualTo(configurationClass2); // --------------------------------------------------------------------- @@ -72,18 +72,18 @@ void verifyEquals() throws Exception { BeanMethod beanMethod_3_1 = beanMethods3.get(1); BeanMethod beanMethod_3_2 = beanMethods3.get(2); - assertThat(beanMethod_1_0.equals(null)).isFalse(); + assertThat(beanMethod_1_0).isNotEqualTo(null); assertThat(beanMethod_1_0).isNotSameAs(beanMethod_2_0); - assertThat(beanMethod_1_0.equals(beanMethod_1_0)).isTrue(); - assertThat(beanMethod_1_0.equals(beanMethod_2_0)).isTrue(); - assertThat(beanMethod_1_1.equals(beanMethod_2_1)).isTrue(); - assertThat(beanMethod_1_2.equals(beanMethod_2_2)).isTrue(); + assertThat(beanMethod_1_0).isEqualTo(beanMethod_1_0); + assertThat(beanMethod_1_0).isEqualTo(beanMethod_2_0); + assertThat(beanMethod_1_1).isEqualTo(beanMethod_2_1); + assertThat(beanMethod_1_2).isEqualTo(beanMethod_2_2); assertThat(beanMethod_1_0.getMetadata().getMethodName()).isEqualTo(beanMethod_3_0.getMetadata().getMethodName()); - assertThat(beanMethod_1_0.equals(beanMethod_3_0)).isFalse(); - assertThat(beanMethod_1_1.equals(beanMethod_3_1)).isFalse(); - assertThat(beanMethod_1_2.equals(beanMethod_3_2)).isFalse(); + assertThat(beanMethod_1_0).isNotEqualTo(beanMethod_3_0); + assertThat(beanMethod_1_1).isNotEqualTo(beanMethod_3_1); + assertThat(beanMethod_1_2).isNotEqualTo(beanMethod_3_2); } @Test 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..96051f161729 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.security.SecureClassLoader; + +import org.junit.jupiter.api.Test; + +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(); + CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader); + Class myClass = parentClassLoader.loadClass(MyConfig.class.getName()); + configurationClassEnhancer.enhance(myClass, parentClassLoader); + Class myReloadedClass = classLoader.loadClass(MyConfig.class.getName()); + Class enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader); + assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader); + } + + + @Configuration + static class MyConfig { + + @Bean + public String myBean() { + return "bean"; + } + } + + + static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader { + + CustomClassLoader(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"); + } + } + +} 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 0f6c5b0640fa..8c91db3cd5fd 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,14 +39,14 @@ * @author Chris Beams * @since 3.1 */ -public class ConfigurationClassPostConstructAndAutowiringTests { +class ConfigurationClassPostConstructAndAutowiringTests { /** * Prior to the fix for SPR-8080, this method would succeed due to ordering of * configuration class registration. */ @Test - public void control() { + void control() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(Config1.class, Config2.class); ctx.refresh(); @@ -62,7 +62,7 @@ public void control() { * configuration class registration. */ @Test - public void originalReproCase() { + void originalReproCase() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(Config2.class, Config1.class); ctx.refresh(); 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 13cb21a2f39e..9b7777cbf9f9 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,7 @@ import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.entry; /** @@ -72,6 +73,7 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Sam Brannen */ class ConfigurationClassPostProcessorAotContributionTests { @@ -99,8 +101,8 @@ void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorWithMapEntr initializer.accept(freshBeanFactory); freshContext.refresh(); assertThat(freshBeanFactory.getBeanPostProcessors()).filteredOn(ImportAwareAotBeanPostProcessor.class::isInstance) - .singleElement().satisfies(postProcessor -> assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class, - ImportConfiguration.class)); + .singleElement().satisfies(postProcessor -> + assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class, ImportConfiguration.class)); freshContext.close(); }); } @@ -117,8 +119,8 @@ void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorAfterApplic freshContext.refresh(); TestAwareCallbackBean bean = freshContext.getBean(TestAwareCallbackBean.class); assertThat(bean.instances).hasSize(2); - assertThat(bean.instances.get(0)).isEqualTo(freshContext); - assertThat(bean.instances.get(1)).isInstanceOfSatisfying(AnnotationMetadata.class, metadata -> + assertThat(bean.instances).element(0).isEqualTo(freshContext); + assertThat(bean.instances).element(1).isInstanceOfSatisfying(AnnotationMetadata.class, metadata -> assertThat(metadata.getClassName()).isEqualTo(TestAwareCallbackConfiguration.class.getName())); freshContext.close(); }); @@ -236,13 +238,14 @@ public int getOrder() { } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { Assert.notNull(this.metadata, "Metadata was not injected"); } } } + @Nested class PropertySourceTests { @@ -264,6 +267,42 @@ void applyToWhenHasPropertySourceInvokePropertySourceProcessor() { }); } + @Test + void propertySourceWithClassPathStarLocationPattern() { + BeanFactoryInitializationAotContribution contribution = + getContribution(PropertySourceWithClassPathStarLocationPatternConfiguration.class); + + // We can effectively only assert that an exception is not thrown; however, + // a WARN-level log message similar to the following should be logged. + // + // Runtime hint registration is not supported for the 'classpath*:' prefix or wildcards + // in @PropertySource locations. Please manually register a resource hint for each property + // source location represented by 'classpath*:org/springframework/context/annotation/*.properties'. + assertThatNoException().isThrownBy(() -> contribution.applyTo(generationContext, beanFactoryInitializationCode)); + + // But we can also ensure that a resource hint was not registered. + assertThat(resource("org/springframework/context/annotation/p1.properties")) + .rejects(generationContext.getRuntimeHints()); + } + + @Test + void propertySourceWithWildcardLocationPattern() { + BeanFactoryInitializationAotContribution contribution = + getContribution(PropertySourceWithWildcardLocationPatternConfiguration.class); + + // We can effectively only assert that an exception is not thrown; however, + // a WARN-level log message similar to the following should be logged. + // + // Runtime hint registration is not supported for the 'classpath*:' prefix or wildcards + // in @PropertySource locations. Please manually register a resource hint for each property + // source location represented by 'classpath:org/springframework/context/annotation/p?.properties'. + assertThatNoException().isThrownBy(() -> contribution.applyTo(generationContext, beanFactoryInitializationCode)); + + // But we can also ensure that a resource hint was not registered. + assertThat(resource("org/springframework/context/annotation/p1.properties")) + .rejects(generationContext.getRuntimeHints()); + } + @Test void applyToWhenHasPropertySourcesInvokesPropertySourceProcessorInOrder() { BeanFactoryInitializationAotContribution contribution = getContribution( @@ -363,8 +402,18 @@ static class PropertySourceWithCustomFactoryConfiguration { } + @Configuration(proxyBeanMethods = false) + @PropertySource("classpath*:org/springframework/context/annotation/*.properties") + static class PropertySourceWithClassPathStarLocationPatternConfiguration { + } + + @Configuration(proxyBeanMethods = false) + @PropertySource("classpath:org/springframework/context/annotation/p?.properties") + static class PropertySourceWithWildcardLocationPatternConfiguration { + } } + @Nested class ConfigurationClassProxyTests { @@ -384,15 +433,14 @@ void processAheadOfTimeFullConfigurationClass() { getRegisteredBean(CglibConfiguration.class))).isNotNull(); } - private RegisteredBean getRegisteredBean(Class bean) { this.beanFactory.registerBeanDefinition("test", new RootBeanDefinition(bean)); this.processor.postProcessBeanFactory(this.beanFactory); return RegisteredBean.of(this.beanFactory, "test"); } - } + @Nullable private BeanFactoryInitializationAotContribution getContribution(Class... types) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -410,8 +458,8 @@ private void assertPostProcessorEntry(BeanPostProcessor postProcessor, Class .containsExactly(entry(key.getName(), value.getName())); } - static class CustomPropertySourcesFactory extends DefaultPropertySourceFactory { + static class CustomPropertySourcesFactory extends DefaultPropertySourceFactory { } } 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 c21783c9c817..c205ac781ab9 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,8 @@ import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; @@ -321,8 +323,8 @@ private void assertSupportForComposedAnnotationWithExclude(RootBeanDefinition be ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.setEnvironment(new StandardEnvironment()); pp.postProcessBeanFactory(beanFactory); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - beanFactory.getBean(SimpleComponent.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> beanFactory.getBean(SimpleComponent.class)); } @Test @@ -372,11 +374,11 @@ 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()); + assertThatExceptionOfType(BeanDefinitionStoreException.class) + .isThrownBy(() -> pp.postProcessBeanFactory(beanFactory)) + .withMessageContaining("bar") + .withMessageContaining("SingletonBeanConfig") + .withMessageContaining(TestBean.class.getName()); } @Test // gh-25430 @@ -429,12 +431,12 @@ void configurationClassesWithInvalidOverridingForProgrammaticCall() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean(Bar.class)) - .withMessageContaining("OverridingSingletonBeanConfig.foo") - .withMessageContaining(ExtendedFoo.class.getName()) - .withMessageContaining(Foo.class.getName()) - .withMessageContaining("InvalidOverridingSingletonBeanConfig"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> beanFactory.getBean(Bar.class)) + .withMessageContaining("OverridingSingletonBeanConfig.foo") + .withMessageContaining(ExtendedFoo.class.getName()) + .withMessageContaining(Foo.class.getName()) + .withMessageContaining("InvalidOverridingSingletonBeanConfig"); } @Test // SPR-15384 @@ -944,8 +946,8 @@ void testSelfReferenceExclusionForFactoryMethodOnSameBean() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfig.class)); beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - beanFactory.preInstantiateSingletons(); + beanFactory.preInstantiateSingletons(); beanFactory.getBean(ServiceBean.class); } @@ -958,8 +960,8 @@ void testConfigWithDefaultMethods() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class)); beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - beanFactory.preInstantiateSingletons(); + beanFactory.preInstantiateSingletons(); beanFactory.getBean(ServiceBean.class); } @@ -972,11 +974,25 @@ void testConfigWithDefaultMethodsUsingAsm() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class.getName())); beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class.getName())); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - beanFactory.preInstantiateSingletons(); + beanFactory.preInstantiateSingletons(); beanFactory.getBean(ServiceBean.class); } + @Test + void testConfigWithFailingInit() { // gh-23343 + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithFailingInit.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(beanFactory::preInstantiateSingletons); + assertThat(beanFactory.containsSingleton("configClass")).isFalse(); + assertThat(beanFactory.containsSingleton("provider")).isFalse(); + } + @Test void testCircularDependency() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); @@ -985,16 +1001,17 @@ void testCircularDependency() { beanFactory.registerBeanDefinition("configClass1", new RootBeanDefinition(A.class)); beanFactory.registerBeanDefinition("configClass2", new RootBeanDefinition(AStrich.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - beanFactory::preInstantiateSingletons) - .withMessageContaining("Circular reference"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(beanFactory::preInstantiateSingletons) + .withMessageContaining("Circular reference"); } @Test void testCircularDependencyWithApplicationContext() { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - new AnnotationConfigApplicationContext(A.class, AStrich.class)) - .withMessageContaining("Circular reference"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(A.class, AStrich.class)) + .withMessageContaining("Circular reference"); } @Test @@ -1048,9 +1065,7 @@ void testEmptyVarargOnBeanMethod() { void testCollectionArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class)); + assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class)); ctx.close(); } @@ -1058,8 +1073,7 @@ void testCollectionArgumentOnBeanMethod() { void testEmptyCollectionArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans.isEmpty()).isTrue(); + assertThat(bean.testBeans).isEmpty(); ctx.close(); } @@ -1067,9 +1081,7 @@ void testEmptyCollectionArgumentOnBeanMethod() { void testMapArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.values().iterator().next()).isSameAs(ctx.getBean(Runnable.class)); + assertThat(bean.testBeans).hasSize(1).containsValue(ctx.getBean(Runnable.class)); ctx.close(); } @@ -1077,8 +1089,7 @@ void testMapArgumentOnBeanMethod() { void testEmptyMapArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans.isEmpty()).isTrue(); + assertThat(bean.testBeans).isEmpty(); ctx.close(); } @@ -1086,9 +1097,7 @@ void testEmptyMapArgumentOnBeanMethod() { void testCollectionInjectionFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class); CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class)); + assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class)); ctx.close(); } @@ -1096,25 +1105,21 @@ void testCollectionInjectionFromSameConfigurationClass() { void testMapInjectionFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class); MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.get("testBean")).isSameAs(ctx.getBean(Runnable.class)); + assertThat(bean.testBeans).containsOnly(Map.entry("testBean", ctx.getBean(Runnable.class))); ctx.close(); } @Test void testBeanLookupFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class); - BeanLookupConfiguration bean = ctx.getBean(BeanLookupConfiguration.class); - assertThat(bean.getTestBean()).isNotNull(); - assertThat(bean.getTestBean()).isSameAs(ctx.getBean(TestBean.class)); + assertThat(ctx.getBean(BeanLookupConfiguration.class).getTestBean()).isSameAs(ctx.getBean(TestBean.class)); ctx.close(); } @Test void testNameClashBetweenConfigurationClassAndBean() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class)); + .isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class)); } @Test @@ -1131,11 +1136,11 @@ void testBeanDefinitionRegistryPostProcessorConfig() { @Order(1) static class SingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1143,11 +1148,11 @@ static class SingletonBeanConfig { @Configuration(proxyBeanMethods = false) static class NonEnhancedSingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1155,11 +1160,13 @@ static class NonEnhancedSingletonBeanConfig { @Configuration static class StaticSingletonBeanConfig { - public static @Bean Foo foo() { + @Bean + public static Foo foo() { return new Foo(); } - public static @Bean Bar bar() { + @Bean + public static Bar bar() { return new Bar(foo()); } } @@ -1168,11 +1175,11 @@ static class StaticSingletonBeanConfig { @Order(2) static class OverridingSingletonBeanConfig { - public @Bean ExtendedFoo foo() { + @Bean public ExtendedFoo foo() { return new ExtendedFoo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1180,7 +1187,7 @@ static class OverridingSingletonBeanConfig { @Configuration static class OverridingAgainSingletonBeanConfig { - public @Bean ExtendedAgainFoo foo() { + @Bean public ExtendedAgainFoo foo() { return new ExtendedAgainFoo(); } } @@ -1188,7 +1195,7 @@ static class OverridingAgainSingletonBeanConfig { @Configuration static class InvalidOverridingSingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } } @@ -1200,11 +1207,11 @@ static class ConfigWithOrderedNestedClasses { @Order(1) static class SingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1213,11 +1220,11 @@ static class SingletonBeanConfig { @Order(2) static class OverridingSingletonBeanConfig { - public @Bean ExtendedFoo foo() { + @Bean public ExtendedFoo foo() { return new ExtendedFoo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1233,11 +1240,11 @@ class SingletonBeanConfig { public SingletonBeanConfig(ConfigWithOrderedInnerClasses other) { } - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1250,11 +1257,11 @@ public OverridingSingletonBeanConfig(ObjectProvider other) other.getObject(); } - public @Bean ExtendedFoo foo() { + @Bean public ExtendedFoo foo() { return new ExtendedFoo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1281,7 +1288,7 @@ public Bar(Foo foo) { @Configuration static class UnloadedConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } } @@ -1289,7 +1296,7 @@ static class UnloadedConfig { @Configuration static class LoadedConfig { - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(new Foo()); } } @@ -1598,7 +1605,7 @@ public Object repoConsumer(Repository repo) { public static class WildcardWithGenericExtendsConfiguration { @Bean - public Repository genericRepo() { + public Repository genericRepo() { return new Repository(); } @@ -1707,7 +1714,7 @@ public String getParameter() { } @Configuration - public static abstract class AbstractConfig { + public abstract static class AbstractConfig { @Bean public ServiceBean serviceBean() { @@ -1758,7 +1765,6 @@ default ServiceBean serviceBean() { } public interface DefaultMethodsConfig extends BaseDefaultMethods { - } @Configuration @@ -1779,6 +1785,29 @@ public void validate() { } } + @Configuration + public static class ConcreteConfigWithFailingInit implements DefaultMethodsConfig, BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Bean + @Override + public ServiceBeanProvider provider() { + return new ServiceBeanProvider(); + } + + @PostConstruct + public void validate() { + beanFactory.getBean("provider"); + throw new IllegalStateException(); + } + } + @Primary public static class ServiceBeanProvider { @@ -1891,7 +1920,7 @@ static class DependingFoo { } } - static abstract class FooFactory { + abstract static class FooFactory { abstract DependingFoo createFoo(BarArgument bar); } @@ -2010,7 +2039,7 @@ private boolean testBean(boolean param) { } @Configuration - static abstract class BeanLookupConfiguration { + abstract static class BeanLookupConfiguration { @Bean public TestBean thing() { 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 5ae586619dfe..ee89a8fa4331 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public class ConfigurationClassWithConditionTests { @Test - public void conditionalOnMissingBeanMatch() throws Exception { + void conditionalOnMissingBeanMatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(BeanOneConfiguration.class, BeanTwoConfiguration.class); ctx.refresh(); @@ -53,7 +53,7 @@ public void conditionalOnMissingBeanMatch() throws Exception { } @Test - public void conditionalOnMissingBeanNoMatch() throws Exception { + void conditionalOnMissingBeanNoMatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(BeanTwoConfiguration.class); ctx.refresh(); @@ -63,7 +63,7 @@ public void conditionalOnMissingBeanNoMatch() throws Exception { } @Test - public void conditionalOnBeanMatch() throws Exception { + void conditionalOnBeanMatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(BeanOneConfiguration.class, BeanThreeConfiguration.class); ctx.refresh(); @@ -72,7 +72,7 @@ public void conditionalOnBeanMatch() throws Exception { } @Test - public void conditionalOnBeanNoMatch() throws Exception { + void conditionalOnBeanNoMatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(BeanThreeConfiguration.class); ctx.refresh(); @@ -81,7 +81,7 @@ public void conditionalOnBeanNoMatch() throws Exception { } @Test - public void metaConditional() throws Exception { + void metaConditional() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigurationWithMetaCondition.class); ctx.refresh(); @@ -89,7 +89,7 @@ public void metaConditional() throws Exception { } @Test - public void metaConditionalWithAsm() throws Exception { + void metaConditionalWithAsm() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.registerBeanDefinition("config", new RootBeanDefinition(ConfigurationWithMetaCondition.class.getName())); ctx.refresh(); @@ -97,7 +97,7 @@ public void metaConditionalWithAsm() throws Exception { } @Test - public void nonConfigurationClass() throws Exception { + void nonConfigurationClass() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(NonConfigurationClass.class); ctx.refresh(); @@ -105,7 +105,7 @@ public void nonConfigurationClass() throws Exception { } @Test - public void nonConfigurationClassWithAsm() throws Exception { + void nonConfigurationClassWithAsm() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.registerBeanDefinition("config", new RootBeanDefinition(NonConfigurationClass.class.getName())); ctx.refresh(); @@ -113,7 +113,7 @@ public void nonConfigurationClassWithAsm() throws Exception { } @Test - public void methodConditional() throws Exception { + void methodConditional() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConditionOnMethodConfiguration.class); ctx.refresh(); @@ -121,7 +121,7 @@ public void methodConditional() throws Exception { } @Test - public void methodConditionalWithAsm() throws Exception { + void methodConditionalWithAsm() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.registerBeanDefinition("config", new RootBeanDefinition(ConditionOnMethodConfiguration.class.getName())); ctx.refresh(); @@ -129,32 +129,30 @@ public void methodConditionalWithAsm() throws Exception { } @Test - public void importsNotCreated() throws Exception { + void importsNotCreated() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ImportsNotCreated.class); ctx.refresh(); } @Test - public void conditionOnOverriddenMethodHonored() { + void conditionOnOverriddenMethodHonored() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithBeanSkipped.class); assertThat(context.getBeansOfType(ExampleBean.class)).isEmpty(); } @Test - public void noConditionOnOverriddenMethodHonored() { + void noConditionOnOverriddenMethodHonored() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithBeanReactivated.class); Map beans = context.getBeansOfType(ExampleBean.class); - assertThat(beans).hasSize(1); - assertThat(beans.keySet().iterator().next()).isEqualTo("baz"); + assertThat(beans).containsOnlyKeys("baz"); } @Test - public void configWithAlternativeBeans() { + void configWithAlternativeBeans() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithAlternativeBeans.class); Map beans = context.getBeansOfType(ExampleBean.class); - assertThat(beans).hasSize(1); - assertThat(beans.keySet().iterator().next()).isEqualTo("baz"); + assertThat(beans).containsOnlyKeys("baz"); } 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 0cbdbe839a11..5176eed7fba4 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,12 +108,12 @@ static class MyFactoryBean implements FactoryBean, InitializingBean { private boolean initialized = false; @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.initialized = true; } @Override - public String getObject() throws Exception { + public String getObject() { return "foo"; } @@ -142,7 +142,7 @@ public MyParameterizedFactoryBean(T obj) { } @Override - public T getObject() throws Exception { + public T getObject() { return obj; } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java similarity index 71% rename from spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java rename to spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java index 74688cd5d81b..28a803f60ad2 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,9 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.core.type.AnnotationMetadata; import static org.assertj.core.api.Assertions.assertThat; @@ -39,51 +41,62 @@ * {@link FactoryBean FactoryBeans} defined in the configuration. * * @author Phillip Webb + * @author Juergen Hoeller */ -public class ConfigurationWithFactoryBeanBeanEarlyDeductionTests { +class ConfigurationWithFactoryBeanEarlyDeductionTests { @Test - public void preFreezeDirect() { + void preFreezeDirect() { assertPreFreeze(DirectConfiguration.class); } @Test - public void postFreezeDirect() { + void postFreezeDirect() { assertPostFreeze(DirectConfiguration.class); } @Test - public void preFreezeGenericMethod() { + void preFreezeGenericMethod() { assertPreFreeze(GenericMethodConfiguration.class); } @Test - public void postFreezeGenericMethod() { + void postFreezeGenericMethod() { assertPostFreeze(GenericMethodConfiguration.class); } @Test - public void preFreezeGenericClass() { + void preFreezeGenericClass() { assertPreFreeze(GenericClassConfiguration.class); } @Test - public void postFreezeGenericClass() { + void postFreezeGenericClass() { assertPostFreeze(GenericClassConfiguration.class); } @Test - public void preFreezeAttribute() { + void preFreezeAttribute() { assertPreFreeze(AttributeClassConfiguration.class); } @Test - public void postFreezeAttribute() { + void postFreezeAttribute() { assertPostFreeze(AttributeClassConfiguration.class); } @Test - public void preFreezeUnresolvedGenericFactoryBean() { + void preFreezeTargetType() { + assertPreFreeze(TargetTypeConfiguration.class); + } + + @Test + void postFreezeTargetType() { + assertPostFreeze(TargetTypeConfiguration.class); + } + + @Test + void preFreezeUnresolvedGenericFactoryBean() { // Covers the case where a @Configuration is picked up via component scanning // and its bean definition only has a String bean class. In such cases // beanDefinition.hasBeanClass() returns false so we need to actually @@ -105,14 +118,13 @@ public void preFreezeUnresolvedGenericFactoryBean() { } } + private void assertPostFreeze(Class configurationClass) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - configurationClass); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass); assertContainsMyBeanName(context); } - private void assertPreFreeze(Class configurationClass, - BeanFactoryPostProcessor... postProcessors) { + private void assertPreFreeze(Class configurationClass, BeanFactoryPostProcessor... postProcessors) { NameCollectingBeanFactoryPostProcessor postProcessor = new NameCollectingBeanFactoryPostProcessor(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); try (context) { @@ -132,41 +144,38 @@ private void assertContainsMyBeanName(String[] names) { assertThat(names).containsExactly("myBean"); } - private static class NameCollectingBeanFactoryPostProcessor - implements BeanFactoryPostProcessor { + + private static class NameCollectingBeanFactoryPostProcessor implements BeanFactoryPostProcessor { private String[] names; @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) - throws BeansException { - this.names = beanFactory.getBeanNamesForType(MyBean.class, true, false); + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ResolvableType typeToMatch = ResolvableType.forClassWithGenerics(MyBean.class, String.class); + this.names = beanFactory.getBeanNamesForType(typeToMatch, true, false); } public String[] getNames() { return this.names; } - } @Configuration static class DirectConfiguration { @Bean - MyBean myBean() { - return new MyBean(); + MyBean myBean() { + return new MyBean<>(); } - } @Configuration static class GenericMethodConfiguration { @Bean - FactoryBean myBean() { - return new TestFactoryBean<>(new MyBean()); + FactoryBean> myBean() { + return new TestFactoryBean<>(new MyBean<>()); } - } @Configuration @@ -176,13 +185,11 @@ static class GenericClassConfiguration { MyFactoryBean myBean() { return new MyFactoryBean(); } - } @Configuration @Import(AttributeClassRegistrar.class) static class AttributeClassConfiguration { - } static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar { @@ -191,16 +198,32 @@ static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition( RawWithAbstractObjectTypeFactoryBean.class).getBeanDefinition(); - definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, MyBean.class); + definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, + ResolvableType.forClassWithGenerics(MyBean.class, String.class)); registry.registerBeanDefinition("myBean", definition); } + } + @Configuration + @Import(TargetTypeRegistrar.class) + static class TargetTypeConfiguration { + } + + static class TargetTypeRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + RootBeanDefinition definition = new RootBeanDefinition(RawWithAbstractObjectTypeFactoryBean.class); + definition.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, + ResolvableType.forClassWithGenerics(MyBean.class, String.class))); + registry.registerBeanDefinition("myBean", definition); + } } abstract static class AbstractMyBean { } - static class MyBean extends AbstractMyBean { + static class MyBean extends AbstractMyBean { } static class TestFactoryBean implements FactoryBean { @@ -212,7 +235,7 @@ public TestFactoryBean(T instance) { } @Override - public T getObject() throws Exception { + public T getObject() { return this.instance; } @@ -220,31 +243,26 @@ public T getObject() throws Exception { public Class getObjectType() { return this.instance.getClass(); } - } - static class MyFactoryBean extends TestFactoryBean { + static class MyFactoryBean extends TestFactoryBean> { public MyFactoryBean() { - super(new MyBean()); + super(new MyBean<>()); } - } static class RawWithAbstractObjectTypeFactoryBean implements FactoryBean { - private final Object object = new MyBean(); - @Override public Object getObject() throws Exception { - return object; + throw new IllegalStateException(); } @Override public Class getObjectType() { return MyBean.class; } - } } 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 e5735e72f915..9efc276b4963 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,28 +29,28 @@ * * @author Stephane Nicoll */ -public class DeferredImportSelectorTests { +class DeferredImportSelectorTests { @Test - public void entryEqualsSameInstance() { + void entryEqualsSameInstance() { AnnotationMetadata metadata = mock(); Group.Entry entry = new Group.Entry(metadata, "com.example.Test"); assertThat(entry).isEqualTo(entry); } @Test - public void entryEqualsSameMetadataAndClassName() { + void entryEqualsSameMetadataAndClassName() { AnnotationMetadata metadata = mock(); assertThat(new Group.Entry(metadata, "com.example.Test")).isEqualTo(new Group.Entry(metadata, "com.example.Test")); } @Test - public void entryEqualDifferentMetadataAndSameClassName() { + void entryEqualDifferentMetadataAndSameClassName() { assertThat(new Group.Entry(mock(), "com.example.Test")).isNotEqualTo(new Group.Entry(mock(), "com.example.Test")); } @Test - public void entryEqualSameMetadataAnDifferentClassName() { + void entryEqualSameMetadataAnDifferentClassName() { AnnotationMetadata metadata = mock(); assertThat(new Group.Entry(metadata, "com.example.AnotherTest")).isNotEqualTo(new Group.Entry(metadata, "com.example.Test")); } 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 c15abc241db2..57e19faf9239 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ package org.springframework.context.annotation; import java.io.Closeable; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ConfigurableApplicationContext; @@ -31,10 +33,10 @@ * @author Juergen Hoeller * @author Stephane Nicoll */ -public class DestroyMethodInferenceTests { +class DestroyMethodInferenceTests { @Test - public void beanMethods() { + void beanMethods() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); WithExplicitDestroyMethod c0 = ctx.getBean(WithExplicitDestroyMethod.class); WithLocalCloseMethod c1 = ctx.getBean("c1", WithLocalCloseMethod.class); @@ -47,6 +49,8 @@ public void beanMethods() { WithInheritedCloseMethod c8 = ctx.getBean("c8", WithInheritedCloseMethod.class); WithDisposableBean c9 = ctx.getBean("c9", WithDisposableBean.class); WithAutoCloseable c10 = ctx.getBean("c10", WithAutoCloseable.class); + WithCompletableFutureMethod c11 = ctx.getBean("c11", WithCompletableFutureMethod.class); + WithReactorMonoMethod c12 = ctx.getBean("c12", WithReactorMonoMethod.class); assertThat(c0.closed).as("c0").isFalse(); assertThat(c1.closed).as("c1").isFalse(); @@ -59,6 +63,8 @@ public void beanMethods() { assertThat(c8.closed).as("c8").isFalse(); assertThat(c9.closed).as("c9").isFalse(); assertThat(c10.closed).as("c10").isFalse(); + assertThat(c11.closed).as("c11").isFalse(); + assertThat(c12.closed).as("c12").isFalse(); ctx.close(); assertThat(c0.closed).as("c0").isTrue(); @@ -72,10 +78,12 @@ public void beanMethods() { assertThat(c8.closed).as("c8").isFalse(); assertThat(c9.closed).as("c9").isTrue(); assertThat(c10.closed).as("c10").isTrue(); + assertThat(c11.closed).as("c11").isTrue(); + assertThat(c12.closed).as("c12").isTrue(); } @Test - public void xml() { + void xml() { ConfigurableApplicationContext ctx = new GenericXmlApplicationContext( getClass(), "DestroyMethodInferenceTests-context.xml"); WithLocalCloseMethod x1 = ctx.getBean("x1", WithLocalCloseMethod.class); @@ -171,6 +179,16 @@ public WithDisposableBean c9() { public WithAutoCloseable c10() { return new WithAutoCloseable(); } + + @Bean + public WithCompletableFutureMethod c11() { + return new WithCompletableFutureMethod(); + } + + @Bean + public WithReactorMonoMethod c12() { + return new WithReactorMonoMethod(); + } } @@ -242,4 +260,38 @@ public void close() { } } + + static class WithCompletableFutureMethod { + + boolean closed = false; + + public CompletableFuture close() { + return CompletableFuture.runAsync(() -> { + try { + Thread.sleep(100); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + closed = true; + }); + } + } + + + static class WithReactorMonoMethod { + + boolean closed = false; + + public Mono close() { + try { + Thread.sleep(100); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return Mono.fromRunnable(() -> closed = true); + } + } + } 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 50b51ae7e7ae..c540d08116c5 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-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class DoubleScanTests extends SimpleScanTests { +class DoubleScanTests extends SimpleScanTests { @Override protected String[] getConfigLocations() { 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 58c8d8753f4c..31d7efa6f514 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import static org.mockito.Mockito.verifyNoInteractions; /** - * Unit tests for @EnableLoadTimeWeaving + * Tests for {@code @EnableLoadTimeWeaving}. * * @author Chris Beams * @since 3.1 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 new file mode 100644 index 000000000000..8c4c67accecd --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/FactoryMethodResolutionTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.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.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.type.AnnotationMetadata; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Andy Wilkinson + */ +class FactoryMethodResolutionTests { + + @Test + void factoryMethodCanBeResolvedWithBeanMetadataCachingEnabled() { + assertThatFactoryMethodCanBeResolved(true); + } + + @Test + void factoryMethodCanBeResolvedWithBeanMetadataCachingDisabled() { + assertThatFactoryMethodCanBeResolved(false); + } + + private void assertThatFactoryMethodCanBeResolved(boolean cache) { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.getBeanFactory().setCacheBeanMetadata(cache); + context.register(ImportSelectorConfiguration.class); + context.refresh(); + BeanDefinition definition = context.getBeanFactory().getMergedBeanDefinition("exampleBean"); + assertThat(((RootBeanDefinition)definition).getResolvedFactoryMethod()).isNotNull(); + } + } + + + @Configuration + @Import(ExampleImportSelector.class) + static class ImportSelectorConfiguration { + } + + + static class ExampleImportSelector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { TestConfiguration.class.getName() }; + } + } + + + @Configuration + static class TestConfiguration { + + @Bean + @ExampleAnnotation + public ExampleBean exampleBean() { + return new ExampleBean(); + } + } + + + static class ExampleBean { + } + + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface ExampleAnnotation { + } + +} 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 new file mode 100644 index 000000000000..ea91c188ecd9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Gh32489Tests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for gh-32489 + * + * @author Stephane Nicoll + */ +public class Gh32489Tests { + + @Test + void resolveFactoryBeansWithWildcard() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.register(SimpleRepositoryFactoriesBeanHolder.class); + context.refresh(); + assertThat(context.getBean(SimpleRepositoryFactoriesBeanHolder.class).repositoryFactoryies) + .containsOnly(context.getBean("&repositoryFactoryBean", SimpleRepositoryFactoryBean.class)); + } + } + + @Test + void resolveFactoryBeansParentInterfaceWithWildcard() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.register(RepositoryFactoriesInformationHolder.class); + context.refresh(); + assertThat(context.getBean(RepositoryFactoriesInformationHolder.class).repositoryFactoresInformation) + .containsOnly(context.getBean("&repositoryFactoryBean", SimpleRepositoryFactoryBean.class)); + } + } + + @Test + void resolveFactoryBeanWithMatchingGenerics() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.register(RepositoryFactoryHolder.class); + context.refresh(); + assertThat(context.getBean(RepositoryFactoryHolder.class).repositoryFactory) + .isEqualTo(context.getBean("&repositoryFactoryBean")); + } + } + + @Test + void provideFactoryBeanWithMatchingGenerics() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.refresh(); + ResolvableType requiredType = ResolvableType.forClassWithGenerics(SimpleRepositoryFactoryBean.class, + EmployeeRepository.class, Long.class); + assertThat(context.getBeanProvider(requiredType)).containsOnly(context.getBean("&repositoryFactoryBean")); + } + } + + @Test + void provideFactoryBeanWithFirstNonMatchingGenerics() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.refresh(); + ResolvableType requiredType = ResolvableType.forClassWithGenerics(SimpleRepositoryFactoryBean.class, + TestBean.class, Long.class); + assertThat(context.getBeanProvider(requiredType)).hasSize(0); + } + } + + @Test + void provideFactoryBeanWithSecondNonMatchingGenerics() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.refresh(); + ResolvableType requiredType = ResolvableType.forClassWithGenerics(SimpleRepositoryFactoryBean.class, + EmployeeRepository.class, String.class); + assertThat(context.getBeanProvider(requiredType)).hasSize(0); + } + } + + @Test + void provideFactoryBeanTargetTypeWithMatchingGenerics() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.refresh(); + ResolvableType requiredType = ResolvableType.forClassWithGenerics(Repository.class, + Employee.class, Long.class); + assertThat(context.getBeanProvider(requiredType)). + containsOnly(context.getBean("repositoryFactoryBean")); + } + } + + @Test + void provideFactoryBeanTargetTypeWithFirstNonMatchingGenerics() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.refresh(); + ResolvableType requiredType = ResolvableType.forClassWithGenerics(Repository.class, + TestBean.class, Long.class); + assertThat(context.getBeanProvider(requiredType)).hasSize(0); + } + } + + @Test + void provideFactoryBeanTargetTypeWithSecondNonMatchingGenerics() { + try (AnnotationConfigApplicationContext context = prepareContext()) { + context.refresh(); + ResolvableType requiredType = ResolvableType.forClassWithGenerics(Repository.class, + Employee.class, String.class); + assertThat(context.getBeanProvider(requiredType)).hasSize(0); + } + } + + private AnnotationConfigApplicationContext prepareContext() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + RootBeanDefinition rbd = new RootBeanDefinition(SimpleRepositoryFactoryBean.class); + rbd.setTargetType(ResolvableType.forClassWithGenerics(SimpleRepositoryFactoryBean.class, + EmployeeRepository.class, Long.class)); + rbd.getConstructorArgumentValues().addIndexedArgumentValue(0, EmployeeRepository.class); + context.registerBeanDefinition("repositoryFactoryBean", rbd); + return context; + } + + + static class SimpleRepositoryFactoriesBeanHolder { + + @Autowired + List> repositoryFactoryies; + } + + static class RepositoryFactoriesInformationHolder { + + @Autowired + List> repositoryFactoresInformation; + } + + static class RepositoryFactoryHolder { + + @Autowired + SimpleRepositoryFactoryBean repositoryFactory; + } + + static class SimpleRepositoryFactoryBean extends RepositoryFactoryBeanSupport { + + private final Class repositoryType; + + public SimpleRepositoryFactoryBean(Class repositoryType) { + this.repositoryType = repositoryType; + } + + @Override + public T getObject() throws Exception { + return BeanUtils.instantiateClass(this.repositoryType); + } + + @Override + public Class getObjectType() { + return this.repositoryType; + } + } + + abstract static class RepositoryFactoryBeanSupport implements FactoryBean, RepositoryFactoryInformation { + } + + interface RepositoryFactoryInformation { + } + + interface Repository {} + + static class EmployeeRepository implements Repository {} + + record Employee(Long id, String name) {} + +} 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 e4f5168c8237..e04987b3bfe8 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -52,6 +51,7 @@ import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; @@ -70,7 +70,7 @@ public class ImportSelectorTests { @BeforeEach - public void cleanup() { + void cleanup() { ImportSelectorTests.importFrom.clear(); SampleImportSelector.cleanup(); TestImportGroup.cleanup(); @@ -78,7 +78,7 @@ public void cleanup() { @Test - public void importSelectors() { + void importSelectors() { DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); context.register(Config.class); @@ -92,7 +92,7 @@ public void importSelectors() { } @Test - public void invokeAwareMethodsInImportSelector() { + void invokeAwareMethodsInImportSelector() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AwareConfig.class); assertThat(SampleImportSelector.beanFactory).isEqualTo(context.getBeanFactory()); assertThat(SampleImportSelector.classLoader).isEqualTo(context.getBeanFactory().getBeanClassLoader()); @@ -101,7 +101,7 @@ public void invokeAwareMethodsInImportSelector() { } @Test - public void correctMetadataOnIndirectImports() { + void correctMetadataOnIndirectImports() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(IndirectConfig.class); String indirectImport = IndirectImport.class.getName(); assertThat(importFrom.get(ImportSelector1.class)).isEqualTo(indirectImport); @@ -115,7 +115,7 @@ public void correctMetadataOnIndirectImports() { } @Test - public void importSelectorsWithGroup() { + void importSelectorsWithGroup() { DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); context.register(GroupedConfig.class); @@ -126,12 +126,11 @@ public void importSelectorsWithGroup() { ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); assertThat(TestImportGroup.instancesCount.get()).isEqualTo(1); - assertThat(TestImportGroup.imports).hasSize(1); - assertThat(TestImportGroup.imports.values().iterator().next()).hasSize(2); + assertThat(TestImportGroup.imports.values()).singleElement().asInstanceOf(LIST).hasSize(2); } @Test - public void importSelectorsSeparateWithGroup() { + void importSelectorsSeparateWithGroup() { DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); context.register(GroupedConfig1.class); @@ -141,14 +140,12 @@ public void importSelectorsSeparateWithGroup() { ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); assertThat(TestImportGroup.instancesCount.get()).isEqualTo(1); - assertThat(TestImportGroup.imports).hasSize(2); - Iterator iterator = TestImportGroup.imports.keySet().iterator(); - assertThat(iterator.next().getClassName()).isEqualTo(GroupedConfig2.class.getName()); - assertThat(iterator.next().getClassName()).isEqualTo(GroupedConfig1.class.getName()); + assertThat(TestImportGroup.imports.keySet().stream().map(AnnotationMetadata::getClassName)) + .containsExactly(GroupedConfig2.class.getName(),GroupedConfig1.class.getName()); } @Test - public void importSelectorsWithNestedGroup() { + void importSelectorsWithNestedGroup() { DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); context.register(ParentConfiguration1.class); @@ -168,7 +165,7 @@ public void importSelectorsWithNestedGroup() { } @Test - public void importSelectorsWithNestedGroupSameDeferredImport() { + void importSelectorsWithNestedGroupSameDeferredImport() { DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); context.register(ParentConfiguration2.class); @@ -187,7 +184,7 @@ public void importSelectorsWithNestedGroupSameDeferredImport() { } @Test - public void invokeAwareMethodsInImportGroup() { + void invokeAwareMethodsInImportGroup() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(GroupedConfig1.class); assertThat(TestImportGroup.beanFactory).isEqualTo(context.getBeanFactory()); assertThat(TestImportGroup.classLoader).isEqualTo(context.getBeanFactory().getBeanClassLoader()); 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 db22532e1bdf..9325ec7412c6 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,10 +26,10 @@ /** * @author Andy Wilkinson */ -public class ImportVersusDirectRegistrationTests { +class ImportVersusDirectRegistrationTests { @Test - public void thingIsNotAvailableWhenOuterConfigurationIsRegisteredDirectly() { + void thingIsNotAvailableWhenOuterConfigurationIsRegisteredDirectly() { try (AnnotationConfigApplicationContext directRegistration = new AnnotationConfigApplicationContext()) { directRegistration.register(AccidentalLiteConfiguration.class); directRegistration.refresh(); @@ -39,7 +39,7 @@ public void thingIsNotAvailableWhenOuterConfigurationIsRegisteredDirectly() { } @Test - public void thingIsNotAvailableWhenOuterConfigurationIsRegisteredWithClassName() { + void thingIsNotAvailableWhenOuterConfigurationIsRegisteredWithClassName() { try (AnnotationConfigApplicationContext directRegistration = new AnnotationConfigApplicationContext()) { directRegistration.registerBeanDefinition("config", new RootBeanDefinition(AccidentalLiteConfiguration.class.getName())); @@ -50,7 +50,7 @@ public void thingIsNotAvailableWhenOuterConfigurationIsRegisteredWithClassName() } @Test - public void thingIsNotAvailableWhenOuterConfigurationIsImported() { + void thingIsNotAvailableWhenOuterConfigurationIsImported() { try (AnnotationConfigApplicationContext viaImport = new AnnotationConfigApplicationContext()) { viaImport.register(Importer.class); viaImport.refresh(); 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 2885637b0605..bd2bb4a41022 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * * @author Chris Beams */ -public class InvalidConfigurationClassDefinitionTests { +class InvalidConfigurationClassDefinitionTests { @Test - public void configurationClassesMayNotBeFinal() { + void configurationClassesMayNotBeFinal() { @Configuration final class Config { } 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 536f0c446eb3..0eecab38364d 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ void lazyResourceInjectionWithField() { FieldResourceInjectionBean bean = ac.getBean("annotatedBean", FieldResourceInjectionBean.class); assertThat(ac.getBeanFactory().containsSingleton("testBean")).isFalse(); - assertThat(bean.getTestBeans().isEmpty()).isFalse(); + assertThat(bean.getTestBeans()).isNotEmpty(); assertThat(bean.getTestBeans().get(0).getName()).isNull(); assertThat(ac.getBeanFactory().containsSingleton("testBean")).isTrue(); TestBean tb = (TestBean) ac.getBean("testBean"); @@ -156,7 +156,7 @@ void lazyOptionalResourceInjectionWithNonExistingTarget() { OptionalFieldResourceInjectionBean bean = (OptionalFieldResourceInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBean()).isNotNull(); assertThat(bean.getTestBeans()).isNotNull(); - assertThat(bean.getTestBeans().isEmpty()).isTrue(); + assertThat(bean.getTestBeans()).isEmpty(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> bean.getTestBean().getName()); } 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 21b4f53a073a..6e069c3a47b2 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 @@ -123,7 +123,7 @@ void twoLevelsDeepWithInheritance() { TestBean pb1 = ctx.getBean("prototypeBean", TestBean.class); TestBean pb2 = ctx.getBean("prototypeBean", TestBean.class); assertThat(pb1).isNotSameAs(pb2); - assertThat(pb1.getFriends().iterator().next()).isNotSameAs(pb2.getFriends().iterator().next()); + assertThat(pb1.getFriends()).element(0).isNotSameAs(pb2.getFriends()); ctx.close(); } @@ -152,7 +152,7 @@ void twoLevelsDeepWithInheritanceThroughImport() { TestBean pb1 = ctx.getBean("prototypeBean", TestBean.class); TestBean pb2 = ctx.getBean("prototypeBean", TestBean.class); assertThat(pb1).isNotSameAs(pb2); - assertThat(pb1.getFriends().iterator().next()).isNotSameAs(pb2.getFriends().iterator().next()); + assertThat(pb1.getFriends()).element(0).isNotSameAs(pb2.getFriends()); ctx.close(); } @@ -181,7 +181,7 @@ void twoLevelsDeepWithInheritanceAndScopedProxy() { TestBean pb1 = ctx.getBean("prototypeBean", TestBean.class); TestBean pb2 = ctx.getBean("prototypeBean", TestBean.class); assertThat(pb1).isNotSameAs(pb2); - assertThat(pb1.getFriends().iterator().next()).isNotSameAs(pb2.getFriends().iterator().next()); + assertThat(pb1.getFriends()).element(0).isNotSameAs(pb2.getFriends()); ctx.close(); } 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 7acc0c7d201d..2a1910220847 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ * * @author Phillip Webb */ -public class ParserStrategyUtilsTests { +class ParserStrategyUtilsTests { @Mock private Environment environment; @@ -66,7 +66,7 @@ void setup() { } @Test - public void instantiateClassWhenHasNoArgsConstructorCallsAware() { + void instantiateClassWhenHasNoArgsConstructorCallsAware() { NoArgsConstructor instance = instantiateClass(NoArgsConstructor.class); assertThat(instance.setEnvironment).isSameAs(this.environment); assertThat(instance.setBeanFactory).isSameAs(this.registry); @@ -75,7 +75,7 @@ public void instantiateClassWhenHasNoArgsConstructorCallsAware() { } @Test - public void instantiateClassWhenHasSingleConstructorInjectsParams() { + void instantiateClassWhenHasSingleConstructorInjectsParams() { ArgsConstructor instance = instantiateClass(ArgsConstructor.class); assertThat(instance.environment).isSameAs(this.environment); assertThat(instance.beanFactory).isSameAs(this.registry); @@ -84,7 +84,7 @@ public void instantiateClassWhenHasSingleConstructorInjectsParams() { } @Test - public void instantiateClassWhenHasSingleConstructorAndAwareInjectsParamsAndCallsAware() { + void instantiateClassWhenHasSingleConstructorAndAwareInjectsParamsAndCallsAware() { ArgsConstructorAndAware instance = instantiateClass(ArgsConstructorAndAware.class); assertThat(instance.environment).isSameAs(this.environment); assertThat(instance.setEnvironment).isSameAs(this.environment); @@ -97,20 +97,20 @@ public void instantiateClassWhenHasSingleConstructorAndAwareInjectsParamsAndCall } @Test - public void instantiateClassWhenHasMultipleConstructorsUsesNoArgsConstructor() { + void instantiateClassWhenHasMultipleConstructorsUsesNoArgsConstructor() { // Remain back-compatible by using the default constructor if there's more than one MultipleConstructors instance = instantiateClass(MultipleConstructors.class); assertThat(instance.usedDefaultConstructor).isTrue(); } @Test - public void instantiateClassWhenHasMultipleConstructorsAndNotDefaultThrowsException() { + void instantiateClassWhenHasMultipleConstructorsAndNotDefaultThrowsException() { assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> instantiateClass(MultipleConstructorsWithNoDefault.class)); } @Test - public void instantiateClassWhenHasUnsupportedParameterThrowsException() { + void instantiateClassWhenHasUnsupportedParameterThrowsException() { assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> instantiateClass(InvalidConstructorParameterType.class)) .withCauseInstanceOf(IllegalStateException.class) @@ -118,7 +118,7 @@ public void instantiateClassWhenHasUnsupportedParameterThrowsException() { } @Test - public void instantiateClassHasSubclassParameterThrowsException() { + void instantiateClassHasSubclassParameterThrowsException() { // To keep the algorithm simple we don't support subtypes assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> instantiateClass(InvalidConstructorParameterSubType.class)) @@ -127,14 +127,14 @@ public void instantiateClassHasSubclassParameterThrowsException() { } @Test - public void instantiateClassWhenHasNoBeanClassLoaderInjectsNull() { + void instantiateClassWhenHasNoBeanClassLoaderInjectsNull() { reset(this.resourceLoader); ArgsConstructor instance = instantiateClass(ArgsConstructor.class); assertThat(instance.beanClassLoader).isNull(); } @Test - public void instantiateClassWhenHasNoBeanClassLoaderDoesNotCallAware() { + void instantiateClassWhenHasNoBeanClassLoaderDoesNotCallAware() { reset(this.resourceLoader); NoArgsConstructor instance = instantiateClass(NoArgsConstructor.class); assertThat(instance.setBeanClassLoader).isNull(); 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 a81a074b2471..4ec292abd3db 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 @@ -22,7 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.Collections; +import java.util.Map; import java.util.Properties; import jakarta.inject.Inject; @@ -31,6 +31,7 @@ import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AliasFor; import org.springframework.core.env.Environment; @@ -130,8 +131,8 @@ void withCustomFactoryAsMeta() { @Test void withUnresolvablePlaceholder() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -162,47 +163,43 @@ void withResolvablePlaceholderAndFactoryBean() { @Test void withEmptyResourceLocations() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithEmptyResourceLocations.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithEmptyResourceLocations.class)) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test void withNameAndMultipleResourceLocations() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNameAndMultipleResourceLocations.class); - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); // p2 should 'win' as it was registered last - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentProperty(ctx, "testbean.name", "p2TestBean"); ctx.close(); } @Test void withMultipleResourceLocations() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithMultipleResourceLocations.class); - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); // p2 should 'win' as it was registered last - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentProperty(ctx, "testbean.name", "p2TestBean"); ctx.close(); } @Test void withRepeatedPropertySourcesInContainerAnnotation() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithPropertySources.class); - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); // p2 should 'win' as it was registered last - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentProperty(ctx, "testbean.name", "p2TestBean"); ctx.close(); } @Test void withRepeatedPropertySources() { try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithRepeatedPropertySourceAnnotations.class)) { - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); // p2 should 'win' as it was registered last - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentProperty(ctx, "testbean.name", "p2TestBean"); } } @@ -213,56 +210,68 @@ void withRepeatedPropertySourcesOnComposedAnnotation() { System.clearProperty(key); try (ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(configClass)) { - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); // p2 should 'win' as it was registered last - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentProperty(ctx, "testbean.name", "p2TestBean"); } System.setProperty(key, "org/springframework/context/annotation"); try (ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(configClass)) { - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p3")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3"); // p3 should 'win' as it was registered last - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p3TestBean"); + assertEnvironmentProperty(ctx, "testbean.name", "p3TestBean"); } finally { System.clearProperty(key); } } + @Test + void multipleComposedPropertySourceAnnotations() { // gh-30941 + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class); + ctx.getBean(MultipleComposedAnnotationsConfig.class); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3", "from.p4", "from.p5"); + // p5 should 'win' as it is registered via the last "locally declared" direct annotation + assertEnvironmentProperty(ctx, "testbean.name", "p5TestBean"); + ctx.close(); + } + + @Test + void multipleResourcesFromPropertySourcePattern() { // gh-21325 + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(ResourcePatternConfig.class); + ctx.getBean(ResourcePatternConfig.class); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3", "from.p4", "from.p5"); + ctx.close(); + } + @Test void withNamedPropertySources() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNamedPropertySources.class); - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); // p2 should 'win' as it was registered last - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentProperty(ctx, "testbean.name", "p2TestBean"); ctx.close(); } @Test void withMissingPropertySource() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class)) - .withCauseInstanceOf(FileNotFoundException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class)) + .withCauseInstanceOf(FileNotFoundException.class); } @Test void withIgnoredPropertySource() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithIgnoredPropertySource.class); - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); ctx.close(); } @Test void withSameSourceImportedInDifferentOrder() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithSameSourceImportedInDifferentOrder.class); - assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); - assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); - assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2"); + assertEnvironmentProperty(ctx, "testbean.name", "p2TestBean"); ctx.close(); } @@ -271,33 +280,44 @@ void orderingWithAndWithoutNameAndMultipleResourceLocations() { // SPR-10820: p2 should 'win' as it was registered last AnnotationConfigApplicationContext ctxWithName = new AnnotationConfigApplicationContext(ConfigWithNameAndMultipleResourceLocations.class); AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(ConfigWithMultipleResourceLocations.class); - assertThat(ctxWithoutName.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); - assertThat(ctxWithName.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertEnvironmentProperty(ctxWithName, "testbean.name", "p2TestBean"); + assertEnvironmentProperty(ctxWithoutName, "testbean.name", "p2TestBean"); ctxWithName.close(); ctxWithoutName.close(); } @Test - void orderingWithAndWithoutNameAndFourResourceLocations() { + void orderingWithFourResourceLocations() { // SPR-12198: p4 should 'win' as it was registered last - AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(ConfigWithFourResourceLocations.class); - assertThat(ctxWithoutName.getEnvironment().getProperty("testbean.name")).isEqualTo("p4TestBean"); - ctxWithoutName.close(); + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithFourResourceLocations.class); + assertEnvironmentProperty(ctx, "testbean.name", "p4TestBean"); + ctx.close(); } @Test - void orderingDoesntReplaceExisting() throws Exception { + void orderingDoesntReplaceExisting() { // SPR-12198: mySource should 'win' as it was registered manually AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(); - MapPropertySource mySource = new MapPropertySource("mine", Collections.singletonMap("testbean.name", "myTestBean")); + MapPropertySource mySource = new MapPropertySource("mine", Map.of("testbean.name", "myTestBean")); ctxWithoutName.getEnvironment().getPropertySources().addLast(mySource); ctxWithoutName.register(ConfigWithFourResourceLocations.class); ctxWithoutName.refresh(); - assertThat(ctxWithoutName.getEnvironment().getProperty("testbean.name")).isEqualTo("myTestBean"); + assertEnvironmentProperty(ctxWithoutName, "testbean.name", "myTestBean"); ctxWithoutName.close(); } + private static void assertEnvironmentContainsProperties(ApplicationContext ctx, String... names) { + for (String name : names) { + assertThat(ctx.getEnvironment().containsProperty(name)).as("environment contains property '%s'", name).isTrue(); + } + } + + private static void assertEnvironmentProperty(ApplicationContext ctx, String name, Object value) { + assertThat(ctx.getEnvironment().getProperty(name)).as("environment property '%s'", name).isEqualTo(value); + } + + @Configuration @PropertySource("classpath:${unresolvable}/p1.properties") static class ConfigWithUnresolvablePlaceholder { @@ -489,6 +509,34 @@ static class ConfigWithRepeatedPropertySourceAnnotations { static class ConfigWithRepeatedPropertySourceAnnotationsOnComposedAnnotation { } + @Retention(RetentionPolicy.RUNTIME) + @PropertySource("classpath:org/springframework/context/annotation/p1.properties") + @interface PropertySource1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertySource("classpath:org/springframework/context/annotation/p2.properties") + @PropertySources({ + @PropertySource("classpath:org/springframework/context/annotation/p3.properties"), + }) + @interface PropertySource23 { + } + + @Configuration + @PropertySource1 + @PropertySource23 + @PropertySources({ + @PropertySource("classpath:org/springframework/context/annotation/p4.properties") + }) + @PropertySource("classpath:org/springframework/context/annotation/p5.properties") + static class MultipleComposedAnnotationsConfig { + } + + + @PropertySource("classpath*:org/springframework/context/annotation/p?.properties") + static class ResourcePatternConfig { + } + @Configuration @PropertySources({ 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 5199f2be92e4..6a31d3bbc228 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-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * @since 3.1 * @see org.springframework.util.ReflectionUtilsTests */ -public class ReflectionUtilsIntegrationTests { +class ReflectionUtilsIntegrationTests { @Test - public void getUniqueDeclaredMethods_withCovariantReturnType_andCglibRewrittenMethodNames() throws Exception { + void getUniqueDeclaredMethods_withCovariantReturnType_andCglibRewrittenMethodNames() { Class cglibLeaf = new ConfigurationClassEnhancer().enhance(Leaf.class, null); int m1MethodCount = 0; Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(cglibLeaf); @@ -54,7 +54,7 @@ public void getUniqueDeclaredMethods_withCovariantReturnType_andCglibRewrittenMe @Configuration - static abstract class Parent { + abstract static class Parent { public abstract Number m1(); } 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 new file mode 100644 index 000000000000..35d65eeefccb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverFieldTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ResourceElementResolver} with fields. + * + * @author Stephane Nicoll + */ +class ResourceElementResolverFieldTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @Test + void resolveWhenFieldIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> ResourceElementResolver.forField("missing").resolve(registeredBean)) + .withMessage("No field 'missing' found on " + TestBean.class.getName()); + } + + @Test + void resolveReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceElementResolver.forField("one") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenResourceNameAndMatchReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceElementResolver.forField("test", "two").resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWheNoMatchFallbackOnType() { + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceElementResolver.forField("one").resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWhenMultipleCandidatesWithNoNameMatchThrowsException() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceElementResolver.forField("test").resolve(registeredBean)) + .isInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()) + .hasMessageContaining("one").hasMessageContaining("two"); + } + + @Test + void resolveWhenNoCandidateMatchingTypeThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceElementResolver.forField("test").resolve(registeredBean)) + .isInstanceOf(NoSuchBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveWhenInvalidMatchingTypeThrowsException() { + this.beanFactory.registerSingleton("count", "counter"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceElementResolver.forField("count").resolve(registeredBean)) + .isInstanceOf(BeanNotOfRequiredTypeException.class) + .hasMessageContaining(Integer.class.getName()) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveAndSetSetsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + ResourceElementResolver.forField("one").resolveAndSet(registeredBean, testBean); + assertThat(testBean.one).isEqualTo("1"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceElementResolver.forField("one").resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + + static class TestBean { + + String one; + + String test; + + Integer count; + } + +} 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 new file mode 100644 index 000000000000..555f082c2776 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverMethodTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.io.InputStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ResourceElementResolver} with methods. + * + * @author Stephane Nicoll + */ +class ResourceElementResolverMethodTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @Test + void resolveWhenMethodIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceElementResolver resolver = ResourceElementResolver.forMethod("missing", InputStream.class); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.resolve(registeredBean)) + .withMessage("Method 'missing' with parameter type 'java.io.InputStream' declared on %s could not be found.", + TestBean.class.getName()); + } + + @Test + void resolveReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceElementResolver resolver = ResourceElementResolver.forMethod("setOne", String.class); + Object resolved = resolver.resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenResourceNameAndMatchReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceElementResolver.forMethod("setTest", String.class, "two").resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWheNoMatchFallbackOnType() { + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceElementResolver.forMethod("setOne", String.class).resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWhenMultipleCandidatesWithNoNameMatchThrowsException() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceElementResolver.forMethod("setTest", String.class).resolve(registeredBean)) + .isInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()) + .hasMessageContaining("one").hasMessageContaining("two"); + } + + @Test + void resolveWhenNoCandidateMatchingTypeThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceElementResolver.forMethod("setTest", String.class).resolve(registeredBean)) + .isInstanceOf(NoSuchBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveWhenInvalidMatchingTypeThrowsException() { + this.beanFactory.registerSingleton("count", "counter"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceElementResolver.forMethod("setCount", Integer.class).resolve(registeredBean)) + .isInstanceOf(BeanNotOfRequiredTypeException.class) + .hasMessageContaining(Integer.class.getName()) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveAndInvokeInvokesMethod() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + ResourceElementResolver.forMethod("setOne", String.class).resolveAndSet(registeredBean, testBean); + assertThat(testBean.one).isEqualTo("1"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceElementResolver.forMethod("setOne", String.class).resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + + 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; + } + + public void setCount(Integer count) { + this.count = count; + } + } + +} 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 9d7be1a225a5..8331d931c7d7 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,10 +31,10 @@ * @author Mark Fisher * @author Juergen Hoeller */ -public class SimpleConfigTests { +class SimpleConfigTests { @Test - public void testFooService() throws Exception { + void testFooService() throws Exception { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getConfigLocations(), getClass()); FooService fooService = ctx.getBean("fooServiceImpl", FooService.class); 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 ee4f47429c90..846a5e5e7f83 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,14 +29,14 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class SimpleScanTests { +class SimpleScanTests { protected String[] getConfigLocations() { return new String[] {"simpleScanTests.xml"}; } @Test - public void testFooService() throws Exception { + void testFooService() { 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 a6002b4ffdce..48cf378bf7ad 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,7 @@ public FooFactoryBean foo() { } @Bean - public String value() throws Exception { + public String value() { String name = foo().getObject().getName(); Assert.state(name != null, "Name cannot be null"); return name; @@ -85,7 +85,7 @@ public String value() throws Exception { @Bean @Conditional(NoBarCondition.class) - public String bar() throws Exception { + public String bar() { return "bar"; } } @@ -115,7 +115,7 @@ protected static class FooFactoryBean implements FactoryBean, InitializingB private Foo foo = new Foo(); @Override - public Foo getObject() throws Exception { + public Foo getObject() { return foo; } @@ -130,7 +130,7 @@ public boolean isSingleton() { } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.foo.name = "foo"; } } 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 c56c347b8cc0..f74a5fc02468 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,33 +27,33 @@ /** * @author Stephane Nicoll */ -public class Spr12278Tests { +class Spr12278Tests { private AnnotationConfigApplicationContext context; @AfterEach - public void close() { + void close() { if (context != null) { context.close(); } } @Test - public void componentSingleConstructor() { + void componentSingleConstructor() { this.context = new AnnotationConfigApplicationContext(BaseConfiguration.class, SingleConstructorComponent.class); assertThat(this.context.getBean(SingleConstructorComponent.class).autowiredName).isEqualTo("foo"); } @Test - public void componentTwoConstructorsNoHint() { + void componentTwoConstructorsNoHint() { this.context = new AnnotationConfigApplicationContext(BaseConfiguration.class, TwoConstructorsComponent.class); assertThat(this.context.getBean(TwoConstructorsComponent.class).name).isEqualTo("fallback"); } @Test - public void componentTwoSpecificConstructorsNoHint() { + void componentTwoSpecificConstructorsNoHint() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> new AnnotationConfigApplicationContext(BaseConfiguration.class, TwoSpecificConstructorsComponent.class)) .withMessageContaining("No default constructor found"); 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 241319a6d1d0..19d2ae6e3e35 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,29 +34,30 @@ /** * @author Stephane Nicoll */ -public class Spr12636Tests { +class Spr12636Tests { private ConfigurableApplicationContext context; @AfterEach - public void closeContext() { + void closeContext() { if (this.context != null) { this.context.close(); } } @Test - public void orderOnImplementation() { + void orderOnImplementation() { this.context = new AnnotationConfigApplicationContext( UserServiceTwo.class, UserServiceOne.class, UserServiceCollector.class); UserServiceCollector bean = this.context.getBean(UserServiceCollector.class); - assertThat(bean.userServices.get(0)).isSameAs(context.getBean("serviceOne", UserService.class)); - assertThat(bean.userServices.get(1)).isSameAs(context.getBean("serviceTwo", UserService.class)); + assertThat(bean.userServices).containsExactly( + context.getBean("serviceOne", UserService.class), + context.getBean("serviceTwo", UserService.class)); } @Test - public void orderOnImplementationWithProxy() { + void orderOnImplementationWithProxy() { this.context = new AnnotationConfigApplicationContext( UserServiceTwo.class, UserServiceOne.class, UserServiceCollector.class, AsyncConfig.class); @@ -67,8 +68,7 @@ public void orderOnImplementationWithProxy() { assertThat(AopUtils.isAopProxy(serviceTwo)).isTrue(); UserServiceCollector bean = this.context.getBean(UserServiceCollector.class); - assertThat(bean.userServices.get(0)).isSameAs(serviceOne); - assertThat(bean.userServices.get(1)).isSameAs(serviceTwo); + assertThat(bean.userServices).containsExactly(serviceOne, serviceTwo); } @Configuration 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 b93d226e61a1..b7041709b142 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -204,7 +204,7 @@ public FinalFactoryBean foo() { } @Bean - public Bar bar() throws Exception { + public Bar bar() { assertThat(foo().isSingleton()).isTrue(); return new Bar(foo().getObject()); } 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 056f8faa8ee7..06e2c87eb313 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-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ * @author Andy Wilkinson * @author Juergen Hoeller */ -public class Spr16217Tests { +class Spr16217Tests { @Test @Disabled("TODO") @@ -37,7 +37,7 @@ public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInRe } @Test - public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInParseConfigurationPhase() { + void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInParseConfigurationPhase() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ParseConfigurationPhaseImportingConfiguration.class)) { context.getBean("someBean"); @@ -45,17 +45,14 @@ public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInPa } @Test - public void baseConfigurationIsIncludedOnceWhenBothConfigurationClassesAreActive() { + void baseConfigurationIsIncludedOnceWhenBothConfigurationClassesAreActive() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.setAllowBeanDefinitionOverriding(false); - context.register(UnconditionalImportingConfiguration.class); - context.refresh(); - try { + try (context) { + context.setAllowBeanDefinitionOverriding(false); + context.register(UnconditionalImportingConfiguration.class); + context.refresh(); context.getBean("someBean"); } - finally { - context.close(); - } } 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 faf5579aac26..93cf350fca70 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,19 +31,19 @@ * * @author Chris Beams */ -public class Spr6602Tests { +class Spr6602Tests { @Test - public void testXmlBehavior() throws Exception { + void testXmlBehavior() throws Exception { doAssertions(new ClassPathXmlApplicationContext("Spr6602Tests-context.xml", Spr6602Tests.class)); } @Test - public void testConfigurationClassBehavior() throws Exception { + void testConfigurationClassBehavior() throws Exception { doAssertions(new AnnotationConfigApplicationContext(FooConfig.class)); } - private void doAssertions(ApplicationContext ctx) throws Exception { + private void doAssertions(ApplicationContext ctx) { Foo foo = ctx.getBean(Foo.class); Bar bar1 = ctx.getBean(Bar.class); @@ -65,7 +65,7 @@ private void doAssertions(ApplicationContext ctx) throws Exception { public static class FooConfig { @Bean - public Foo foo() throws Exception { + public Foo foo() { return new Foo(barFactory().getObject()); } @@ -93,7 +93,7 @@ public static class Bar { public static class BarFactory implements FactoryBean { @Override - public Bar getObject() throws Exception { + public Bar getObject() { return new Bar(); } 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 475ff6d00525..bc6205794c00 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for SPR-8954, in which a custom {@link InstantiationAwareBeanPostProcessor} + * Tests for SPR-8954, in which a custom {@link InstantiationAwareBeanPostProcessor} * forces the predicted type of a FactoryBean, effectively preventing retrieval of the * bean from calls to #getBeansOfType(FactoryBean.class). The implementation of * {@link AbstractBeanFactory#isFactoryBean(String, RootBeanDefinition)} now ensures @@ -44,7 +44,7 @@ public class Spr8954Tests { @Test - public void repro() { + void repro() { AnnotationConfigApplicationContext bf = new AnnotationConfigApplicationContext(); bf.registerBeanDefinition("fooConfig", new RootBeanDefinition(FooConfig.class)); bf.getBeanFactory().addBeanPostProcessor(new PredictingBPP()); @@ -57,16 +57,14 @@ public void repro() { @SuppressWarnings("rawtypes") Map fbBeans = bf.getBeansOfType(FactoryBean.class); - assertThat(fbBeans.size()).isEqualTo(1); - assertThat(fbBeans.keySet().iterator().next()).isEqualTo("&foo"); + assertThat(fbBeans).containsOnlyKeys("&foo"); Map aiBeans = bf.getBeansOfType(AnInterface.class); - assertThat(aiBeans.size()).isEqualTo(1); - assertThat(aiBeans.keySet().iterator().next()).isEqualTo("&foo"); + assertThat(aiBeans).containsOnlyKeys("&foo"); } @Test - public void findsBeansByTypeIfNotInstantiated() { + void findsBeansByTypeIfNotInstantiated() { AnnotationConfigApplicationContext bf = new AnnotationConfigApplicationContext(); bf.registerBeanDefinition("fooConfig", new RootBeanDefinition(FooConfig.class)); bf.getBeanFactory().addBeanPostProcessor(new PredictingBPP()); @@ -76,12 +74,10 @@ public void findsBeansByTypeIfNotInstantiated() { @SuppressWarnings("rawtypes") Map fbBeans = bf.getBeansOfType(FactoryBean.class); - assertThat(fbBeans.size()).isEqualTo(1); - assertThat(fbBeans.keySet().iterator().next()).isEqualTo("&foo"); + assertThat(fbBeans).containsOnlyKeys("&foo"); Map aiBeans = bf.getBeansOfType(AnInterface.class); - assertThat(aiBeans.size()).isEqualTo(1); - assertThat(aiBeans.keySet().iterator().next()).isEqualTo("&foo"); + assertThat(aiBeans).containsOnlyKeys("&foo"); } @@ -128,7 +124,7 @@ static class PredictingBPP implements SmartInstantiationAwareBeanPostProcessor { @Override public Class predictBeanType(Class beanClass, String beanName) { - return FactoryBean.class.isAssignableFrom(beanClass) ? PredictedType.class : null; + return (FactoryBean.class.isAssignableFrom(beanClass) ? PredictedType.class : null); } @Override 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 9a9795476579..00cd00e3b8cf 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.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.Value; @@ -43,6 +44,7 @@ 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; @@ -91,7 +93,7 @@ void testAutowiredConfigurationMethodDependenciesWithOptionalAndNotAvailable() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( OptionalAutowiredMethodConfig.class); - assertThat(context.getBeansOfType(Colour.class).isEmpty()).isTrue(); + assertThat(context.getBeansOfType(Colour.class)).isEmpty(); assertThat(context.getBean(TestBean.class).getName()).isEmpty(); context.close(); } @@ -183,14 +185,22 @@ void testValueInjectionWithProviderMethodArguments() { context.close(); } + @Test + void testValueInjectionWithAccidentalAutowiredAnnotations() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithAccidentalAutowiredAnnotations.class); + doTestValueInjection(context); + context.close(); + } + private void doTestValueInjection(BeanFactory context) { System.clearProperty("myProp"); TestBean testBean = context.getBean("testBean", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); testBean = context.getBean("testBean2", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); System.setProperty("myProp", "foo"); @@ -203,10 +213,10 @@ private void doTestValueInjection(BeanFactory context) { System.clearProperty("myProp"); testBean = context.getBean("testBean", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); testBean = context.getBean("testBean2", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); } @Test @@ -281,7 +291,7 @@ public TestBean testBean(Optional colour, Optional> colours return new TestBean(""); } else { - return new TestBean(colour.get().toString() + "-" + colours.get().get(0).toString()); + return new TestBean(colour.get() + "-" + colours.get().get(0).toString()); } } } @@ -494,6 +504,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/BeanAnnotationAttributePropagationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java index e334bddff55a..fe3f8b4a69d6 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,10 +43,10 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class BeanAnnotationAttributePropagationTests { +class BeanAnnotationAttributePropagationTests { @Test - public void autowireCandidateMetadataIsPropagated() { + void autowireCandidateMetadataIsPropagated() { @Configuration class Config { @Bean(autowireCandidate=false) Object foo() { return null; } } @@ -55,7 +55,7 @@ public void autowireCandidateMetadataIsPropagated() { } @Test - public void initMethodMetadataIsPropagated() { + void initMethodMetadataIsPropagated() { @Configuration class Config { @Bean(initMethod="start") Object foo() { return null; } } @@ -64,7 +64,7 @@ public void initMethodMetadataIsPropagated() { } @Test - public void destroyMethodMetadataIsPropagated() { + void destroyMethodMetadataIsPropagated() { @Configuration class Config { @Bean(destroyMethod="destroy") Object foo() { return null; } } @@ -73,7 +73,7 @@ public void destroyMethodMetadataIsPropagated() { } @Test - public void dependsOnMetadataIsPropagated() { + void dependsOnMetadataIsPropagated() { @Configuration class Config { @Bean() @DependsOn({"bar", "baz"}) Object foo() { return null; } } @@ -82,7 +82,7 @@ public void dependsOnMetadataIsPropagated() { } @Test - public void primaryMetadataIsPropagated() { + void primaryMetadataIsPropagated() { @Configuration class Config { @Primary @Bean Object foo() { return null; } @@ -92,7 +92,7 @@ public void primaryMetadataIsPropagated() { } @Test - public void primaryMetadataIsFalseByDefault() { + void primaryMetadataIsFalseByDefault() { @Configuration class Config { @Bean Object foo() { return null; } } @@ -101,7 +101,7 @@ public void primaryMetadataIsFalseByDefault() { } @Test - public void lazyMetadataIsPropagated() { + void lazyMetadataIsPropagated() { @Configuration class Config { @Lazy @Bean Object foo() { return null; } @@ -111,7 +111,7 @@ public void lazyMetadataIsPropagated() { } @Test - public void lazyMetadataIsFalseByDefault() { + void lazyMetadataIsFalseByDefault() { @Configuration class Config { @Bean Object foo() { return null; } } @@ -120,7 +120,7 @@ public void lazyMetadataIsFalseByDefault() { } @Test - public void defaultLazyConfigurationPropagatesToIndividualBeans() { + void defaultLazyConfigurationPropagatesToIndividualBeans() { @Lazy @Configuration class Config { @Bean Object foo() { return null; } } @@ -129,7 +129,7 @@ public void defaultLazyConfigurationPropagatesToIndividualBeans() { } @Test - public void eagerBeanOverridesDefaultLazyConfiguration() { + void eagerBeanOverridesDefaultLazyConfiguration() { @Lazy @Configuration class Config { @Lazy(false) @Bean Object foo() { return null; } } @@ -138,7 +138,7 @@ public void eagerBeanOverridesDefaultLazyConfiguration() { } @Test - public void eagerConfigurationProducesEagerBeanDefinitions() { + void eagerConfigurationProducesEagerBeanDefinitions() { @Lazy(false) @Configuration class Config { // will probably never happen, doesn't make much sense @Bean Object foo() { return null; } } 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 9c715f1d25f3..f0e66428d32a 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-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -143,6 +143,12 @@ void finalBeanMethod() { initBeanFactory(ConfigWithFinalBean.class)); } + @Test // gh-31007 + void voidBeanMethod() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> + initBeanFactory(ConfigWithVoidBean.class)); + } + @Test void simplestPossibleConfig() { BeanFactory factory = initBeanFactory(SimplestPossibleConfig.class); @@ -204,7 +210,7 @@ void configurationWithNullReference() { BeanFactory factory = initBeanFactory(ConfigWithNullReference.class); TestBean foo = factory.getBean("foo", TestBean.class); - assertThat(factory.getBean("bar").equals(null)).isTrue(); + assertThat(factory.getBean("bar")).isEqualTo(null); assertThat(foo.getSpouse()).isNull(); } @@ -426,16 +432,24 @@ public Set get() { @Configuration static class ConfigWithFinalBean { - public final @Bean TestBean testBean() { + @Bean public final TestBean testBean() { return new TestBean(); } } + @Configuration + static class ConfigWithVoidBean { + + @Bean public void testBean() { + } + } + + @Configuration static class SimplestPossibleConfig { - public @Bean String stringBean() { + @Bean public String stringBean() { return "foo"; } } @@ -444,11 +458,11 @@ static class SimplestPossibleConfig { @Configuration static class ConfigWithNonSpecificReturnTypes { - public @Bean Object stringBean() { + @Bean public Object stringBean() { return "foo"; } - public @Bean FactoryBean factoryBean() { + @Bean public FactoryBean factoryBean() { ListFactoryBean fb = new ListFactoryBean(); fb.setSourceList(Arrays.asList("element1", "element2")); return fb; @@ -459,13 +473,13 @@ static class ConfigWithNonSpecificReturnTypes { @Configuration static class ConfigWithPrototypeBean { - public @Bean TestBean foo() { + @Bean public TestBean foo() { TestBean foo = new SpousyTestBean("foo"); foo.setSpouse(bar()); return foo; } - public @Bean TestBean bar() { + @Bean public TestBean bar() { TestBean bar = new SpousyTestBean("bar"); bar.setSpouse(baz()); return bar; @@ -605,15 +619,15 @@ static class ConfigWithFunctionalRegistration { void register(GenericApplicationContext ctx) { ctx.registerBean("spouse", TestBean.class, () -> new TestBean("functional")); - Supplier testBeanSupplier = () -> new TestBean(ctx.getBean("spouse", TestBean.class)); - ctx.registerBean(TestBean.class, - testBeanSupplier, + Supplier testBeanSupplier = + () -> new TestBean(ctx.getBean("spouse", TestBean.class)); + ctx.registerBean(TestBean.class, testBeanSupplier, bd -> bd.setPrimary(true)); } @Bean - public NestedTestBean nestedTestBean(TestBean testBean) { - return new NestedTestBean(testBean.getSpouse().getName()); + public NestedTestBean nestedTestBean(TestBean spouse) { + return new NestedTestBean(spouse.getSpouse().getName()); } } 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 0dfcb05c2f90..30c2eb5d1be7 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class ConfigurationClassWithPlaceholderConfigurerBeanTests { +class ConfigurationClassWithPlaceholderConfigurerBeanTests { /** * Test which proves that a non-static property placeholder bean cannot be declared 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 7e97aaa2d055..4085cf39b447 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AliasFor; import static org.assertj.core.api.Assertions.assertThat; @@ -62,9 +63,12 @@ TestBean b() { } - @Configuration @Retention(RetentionPolicy.RUNTIME) + @Configuration @interface TestConfiguration { + + @AliasFor(annotation = Configuration.class) String value() default ""; } + } 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 240640c6864d..861754de3a0d 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ public class ImportAnnotationDetectionTests { @Test - public void multipleMetaImportsAreProcessed() { + void multipleMetaImportsAreProcessed() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(MultiMetaImportConfig.class); ctx.refresh(); @@ -53,7 +53,7 @@ public void multipleMetaImportsAreProcessed() { } @Test - public void localAndMetaImportsAreProcessed() { + void localAndMetaImportsAreProcessed() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(MultiMetaImportConfigWithLocalImport.class); ctx.refresh(); @@ -63,7 +63,7 @@ public void localAndMetaImportsAreProcessed() { } @Test - public void localImportIsProcessedLast() { + void localImportIsProcessedLast() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(MultiMetaImportConfigWithLocalImportWithBeanOverride.class); ctx.refresh(); @@ -73,7 +73,7 @@ public void localImportIsProcessedLast() { } @Test - public void importFromBean() throws Exception { + void importFromBean() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ImportFromBean.class); ctx.refresh(); 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 12fe608bbb06..565b58e9d805 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-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,10 +42,10 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class ImportResourceTests { +class ImportResourceTests { @Test - public void importXml() { + 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(); @@ -55,14 +55,14 @@ public void importXml() { } @Test - public void importXmlIsInheritedFromSuperclassDeclarations() { + void importXmlIsInheritedFromSuperclassDeclarations() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class); assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue(); ctx.close(); } @Test - public void importXmlIsMergedFromSuperclassDeclarations() { + 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(); @@ -70,7 +70,7 @@ public void importXmlIsMergedFromSuperclassDeclarations() { } @Test - public void importXmlWithNamespaceConfig() { + void importXmlWithNamespaceConfig() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class); Object bean = ctx.getBean("proxiedXmlBean"); assertThat(AopUtils.isAopProxy(bean)).isTrue(); @@ -78,7 +78,7 @@ public void importXmlWithNamespaceConfig() { } @Test - public void importXmlWithOtherConfigurationClass() { + 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(); @@ -88,7 +88,7 @@ public void importXmlWithOtherConfigurationClass() { } @Test - public void importWithPlaceholder() throws Exception { + void importWithPlaceholder() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); PropertySource propertySource = new MapPropertySource("test", Collections. singletonMap("test", "springframework")); @@ -100,7 +100,7 @@ public void importWithPlaceholder() throws Exception { } @Test - public void importXmlWithAutowiredConfig() { + void importXmlWithAutowiredConfig() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class); String name = ctx.getBean("xmlBeanName", String.class); assertThat(name).isEqualTo("xml.declared"); @@ -108,7 +108,7 @@ public void importXmlWithAutowiredConfig() { } @Test - public void importNonXmlResource() { + void importNonXmlResource() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class); assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); ctx.close(); @@ -120,7 +120,7 @@ public void importNonXmlResource() { static class ImportXmlConfig { @Value("${name}") private String name; - public @Bean TestBean javaDeclaredBean() { + @Bean public TestBean javaDeclaredBean() { return new TestBean(this.name); } } @@ -160,7 +160,7 @@ static class ImportXmlWithConfigurationClass { static class ImportXmlAutowiredConfig { @Autowired TestBean xmlDeclaredBean; - public @Bean String xmlBeanName() { + @Bean public String xmlBeanName() { return xmlDeclaredBean.getName(); } } 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 52755bd7e5f2..0c54aabd753e 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 2012-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,12 +33,12 @@ /** * @author Andy Wilkinson */ -public class ImportWithConditionTests { +class ImportWithConditionTests { private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @Test - public void conditionalThenUnconditional() throws Exception { + void conditionalThenUnconditional() { this.context.register(ConditionalThenUnconditional.class); this.context.refresh(); assertThat(this.context.containsBean("beanTwo")).isFalse(); @@ -46,7 +46,7 @@ public void conditionalThenUnconditional() throws Exception { } @Test - public void unconditionalThenConditional() throws Exception { + void unconditionalThenConditional() { this.context.register(UnconditionalThenConditional.class); this.context.refresh(); assertThat(this.context.containsBean("beanTwo")).isFalse(); 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 d407d046b5ed..7c82bcc0e233 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ * @author Costin Leau * @author Chris Beams */ -public class ScopingTests { +class ScopingTests { public static String flag = "1"; @@ -61,13 +61,13 @@ public class ScopingTests { @BeforeEach - public void setUp() throws Exception { + void setUp() { customScope = new CustomScope(); ctx = createContext(ScopedConfigurationClass.class); } @AfterEach - public void tearDown() throws Exception { + void tearDown() { if (ctx != null) { ctx.close(); } @@ -86,16 +86,16 @@ private GenericApplicationContext createContext(Class configClass) { @Test - public void testScopeOnClasses() throws Exception { + void testScopeOnClasses() { genericTestScope("scopedClass"); } @Test - public void testScopeOnInterfaces() throws Exception { + void testScopeOnInterfaces() { genericTestScope("scopedInterface"); } - private void genericTestScope(String beanName) throws Exception { + private void genericTestScope(String beanName) { String message = "scope is ignored"; Object bean1 = ctx.getBean(beanName); Object bean2 = ctx.getBean(beanName); @@ -130,7 +130,7 @@ private void genericTestScope(String beanName) throws Exception { } @Test - public void testSameScopeOnDifferentBeans() throws Exception { + void testSameScopeOnDifferentBeans() { Object beanAInScope = ctx.getBean("scopedClass"); Object beanBInScope = ctx.getBean("scopedInterface"); @@ -147,7 +147,7 @@ public void testSameScopeOnDifferentBeans() throws Exception { } @Test - public void testRawScopes() throws Exception { + void testRawScopes() { String beanName = "scopedProxyInterface"; // get hidden bean @@ -158,7 +158,7 @@ public void testRawScopes() throws Exception { } @Test - public void testScopedProxyConfiguration() throws Exception { + void testScopedProxyConfiguration() { TestBean singleton = (TestBean) ctx.getBean("singletonWithScopedInterfaceDep"); ITestBean spouse = singleton.getSpouse(); boolean condition = spouse instanceof ScopedObject; @@ -191,7 +191,7 @@ public void testScopedProxyConfiguration() throws Exception { } @Test - public void testScopedProxyConfigurationWithClasses() throws Exception { + void testScopedProxyConfigurationWithClasses() { TestBean singleton = (TestBean) ctx.getBean("singletonWithScopedClassDep"); ITestBean spouse = singleton.getSpouse(); boolean condition = spouse instanceof ScopedObject; 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 a8237a1e132c..2e57c5ebb5d2 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,10 +31,10 @@ * @author Oliver Gierke * @author Phillip Webb */ -public class Spr10668Tests { +class Spr10668Tests { @Test - public void testSelfInjectHierarchy() throws Exception { + void testSelfInjectHierarchy() { 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 279d520fa595..1907f3ccec1b 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ /** * @author Phillip Webb */ -public class Spr10744Tests { +class Spr10744Tests { private static int createCount = 0; @@ -41,7 +41,7 @@ public class Spr10744Tests { @Test - public void testSpr10744() throws Exception { + void testSpr10744() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.getBeanFactory().registerScope("myTestScope", new MyTestScope()); context.register(MyTestConfiguration.class); @@ -122,7 +122,7 @@ public Foo foo() { static class MyTestConfiguration extends MyConfiguration { @Bean - @Scope(value = "myTestScope", proxyMode = ScopedProxyMode.TARGET_CLASS) + @Scope(value = "myTestScope", proxyMode = ScopedProxyMode.TARGET_CLASS) @Override public Foo foo() { return new Foo(); 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 4a8ff2b57e6e..f9d0574d552a 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ * @author Juergen Hoeller * @since 3.0 */ -public class SpringAtInjectTckTests { +class SpringAtInjectTckTests { @SuppressWarnings("unchecked") public static Test suite() { 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 c7a32b22b6e6..ad7245354f2c 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-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,11 +31,11 @@ /** * @author Rob Winch */ -public class Spr10546Tests { +class Spr10546Tests { private ConfigurableApplicationContext context; @AfterEach - public void closeContext() { + void closeContext() { if (context != null) { context.close(); } @@ -44,7 +44,7 @@ public void closeContext() { // These fail prior to fixing SPR-10546 @Test - public void enclosingConfigFirstParentDefinesBean() { + void enclosingConfigFirstParentDefinesBean() { assertLoadsMyBean(AEnclosingConfig.class,AEnclosingConfig.ChildConfig.class); } @@ -59,7 +59,7 @@ public void enclosingConfigFirstParentDefinesBean() { * classpath scanning implementation being used by the author of this test. */ @Test - public void enclosingConfigFirstParentDefinesBeanWithScanning() { + void enclosingConfigFirstParentDefinesBeanWithScanning() { AnnotationConfigApplicationContext ctx= new AnnotationConfigApplicationContext(); context = ctx; ctx.scan(AEnclosingConfig.class.getPackage().getName()); @@ -68,7 +68,7 @@ public void enclosingConfigFirstParentDefinesBeanWithScanning() { } @Test - public void enclosingConfigFirstParentDefinesBeanWithImportResource() { + void enclosingConfigFirstParentDefinesBeanWithImportResource() { assertLoadsMyBean(AEnclosingWithImportResourceConfig.class,AEnclosingWithImportResourceConfig.ChildConfig.class); } @@ -79,7 +79,7 @@ public static class ChildConfig extends ParentWithImportResourceConfig {} } @Test - public void enclosingConfigFirstParentDefinesBeanWithComponentScan() { + void enclosingConfigFirstParentDefinesBeanWithComponentScan() { assertLoadsMyBean(AEnclosingWithComponentScanConfig.class,AEnclosingWithComponentScanConfig.ChildConfig.class); } @@ -90,7 +90,7 @@ public static class ChildConfig extends ParentWithComponentScanConfig {} } @Test - public void enclosingConfigFirstParentWithParentDefinesBean() { + void enclosingConfigFirstParentWithParentDefinesBean() { assertLoadsMyBean(AEnclosingWithGrandparentConfig.class,AEnclosingWithGrandparentConfig.ChildConfig.class); } @@ -101,7 +101,7 @@ public static class ChildConfig extends ParentWithParentConfig {} } @Test - public void importChildConfigThenChildConfig() { + void importChildConfigThenChildConfig() { assertLoadsMyBean(ImportChildConfig.class,ChildConfig.class); } @@ -116,7 +116,7 @@ static class ImportChildConfig {} // These worked prior, but validating they continue to work @Test - public void enclosingConfigFirstParentDefinesBeanWithImport() { + void enclosingConfigFirstParentDefinesBeanWithImport() { assertLoadsMyBean(AEnclosingWithImportConfig.class,AEnclosingWithImportConfig.ChildConfig.class); } @@ -127,17 +127,17 @@ public static class ChildConfig extends ParentWithImportConfig {} } @Test - public void childConfigFirst() { + void childConfigFirst() { assertLoadsMyBean(AEnclosingConfig.ChildConfig.class, AEnclosingConfig.class); } @Test - public void enclosingConfigOnly() { + void enclosingConfigOnly() { assertLoadsMyBean(AEnclosingConfig.class); } @Test - public void childConfigOnly() { + void childConfigOnly() { assertLoadsMyBean(AEnclosingConfig.ChildConfig.class); } 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 d9b8044b3fd0..49f965c79938 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-2014 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,10 @@ * * @author Phillip Webb */ -public class Spr12233Tests { +class Spr12233Tests { @Test - public void spr12233() throws Exception { + void spr12233() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(PropertySourcesPlaceholderConfigurer.class); ctx.register(ImportConfiguration.class); 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 dcf9a59a59bc..ed9b70285a1d 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ static class TestImport implements ImportBeanDefinitionRegistrar { private static AtomicInteger scanned = new AtomicInteger(); @Override - public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { if (scanned.get() > 0) { throw new IllegalStateException("Already scanned"); } 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 1e751e3471ea..743ad5773733 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-2013 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ public class FactoryMethodComponent { private int i; - public static TestBean nullInstance() { + public static TestBean nullInstance() { return null; } 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 3e983041583f..db0cc8354fc6 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 @@ -22,9 +22,13 @@ import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.GenerationContext; @@ -48,6 +52,8 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.Employee; +import org.springframework.beans.testfixture.beans.Pet; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Implementation; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.One; @@ -58,6 +64,7 @@ import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; 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.AutowiredComponent; import org.springframework.context.testfixture.context.annotation.AutowiredGenericTemplate; import org.springframework.context.testfixture.context.annotation.CglibConfiguration; @@ -69,12 +76,16 @@ import org.springframework.context.testfixture.context.annotation.LazyAutowiredMethodComponent; import org.springframework.context.testfixture.context.annotation.LazyConstructorArgumentComponent; 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.PropertySourceConfiguration; import org.springframework.context.testfixture.context.annotation.QualifierConfiguration; +import org.springframework.context.testfixture.context.annotation.ResourceComponent; import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.Compiled; @@ -103,164 +114,234 @@ void processAheadOfTimeWhenHasSimpleBean() { }); } - @Test - void processAheadOfTimeWhenHasAutowiring() { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.registerBeanDefinition(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, - BeanDefinitionBuilder - .rootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); - applicationContext.registerBeanDefinition("autowiredComponent", new RootBeanDefinition(AutowiredComponent.class)); - applicationContext.registerBeanDefinition("number", - BeanDefinitionBuilder - .rootBeanDefinition(Integer.class, "valueOf") - .addConstructorArgValue("42").getBeanDefinition()); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("autowiredComponent", "number"); - AutowiredComponent bean = freshApplicationContext.getBean(AutowiredComponent.class); - assertThat(bean.getEnvironment()).isSameAs(freshApplicationContext.getEnvironment()); - assertThat(bean.getCounter()).isEqualTo(42); - }); - } + @Nested + class Autowiring { - @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); - assertThat(bean).hasFieldOrPropertyWithValue("genericTemplate", applicationContext.getBean("genericTemplate")); - }); - } + @Test + void processAheadOfTimeWhenHasAutowiring() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + registerBeanPostProcessor(applicationContext, + 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"); + AutowiredComponent bean = freshApplicationContext.getBean(AutowiredComponent.class); + assertThat(bean.getEnvironment()).isSameAs(freshApplicationContext.getEnvironment()); + assertThat(bean.getCounter()).isEqualTo(42); + }); + } - @Test - void processAheadOfTimeWhenHasLazyAutowiringOnField() { - testAutowiredComponent(LazyAutowiredFieldComponent.class, (bean, generationContext) -> { - Environment environment = bean.getEnvironment(); - assertThat(environment).isInstanceOf(Proxy.class); - ResourceLoader resourceLoader = bean.getResourceLoader(); - assertThat(resourceLoader).isNotInstanceOf(Proxy.class); - RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); - assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> - assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( - environment.getClass().getInterfaces()))); + @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); + assertThat(bean).hasFieldOrPropertyWithValue("genericTemplate", applicationContext.getBean("genericTemplate")); + }); + } - }); - } + @Test + void processAheadOfTimeWhenHasLazyAutowiringOnField() { + testAutowiredComponent(LazyAutowiredFieldComponent.class, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isNotInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + environment.getClass().getInterfaces()))); - @Test - void processAheadOfTimeWhenHasLazyAutowiringOnMethod() { - testAutowiredComponent(LazyAutowiredMethodComponent.class, (bean, generationContext) -> { - Environment environment = bean.getEnvironment(); - assertThat(environment).isNotInstanceOf(Proxy.class); - ResourceLoader resourceLoader = bean.getResourceLoader(); - assertThat(resourceLoader).isInstanceOf(Proxy.class); - RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(Environment.class)); - assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> - assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( - resourceLoader.getClass().getInterfaces()))); - }); - } + }); + } - @Test - void processAheadOfTimeWhenHasLazyAutowiringOnConstructor() { - testAutowiredComponent(LazyConstructorArgumentComponent.class, (bean, generationContext) -> { - Environment environment = bean.getEnvironment(); - assertThat(environment).isInstanceOf(Proxy.class); - ResourceLoader resourceLoader = bean.getResourceLoader(); - assertThat(resourceLoader).isNotInstanceOf(Proxy.class); - RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); - assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> - assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( - environment.getClass().getInterfaces()))); - }); - } + @Test + void processAheadOfTimeWhenHasLazyAutowiringOnMethod() { + testAutowiredComponent(LazyAutowiredMethodComponent.class, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isNotInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(Environment.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + resourceLoader.getClass().getInterfaces()))); + }); + } - @Test - void processAheadOfTimeWhenHasLazyAutowiringOnFactoryMethod() { - RootBeanDefinition bd = new RootBeanDefinition(LazyFactoryMethodArgumentComponent.class); - bd.setFactoryMethodName("of"); - testAutowiredComponent(LazyFactoryMethodArgumentComponent.class, bd, (bean, generationContext) -> { - Environment environment = bean.getEnvironment(); - assertThat(environment).isInstanceOf(Proxy.class); - ResourceLoader resourceLoader = bean.getResourceLoader(); - assertThat(resourceLoader).isNotInstanceOf(Proxy.class); - RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); - assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> - assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( - environment.getClass().getInterfaces()))); - }); - } + @Test + void processAheadOfTimeWhenHasLazyAutowiringOnConstructor() { + testAutowiredComponent(LazyConstructorArgumentComponent.class, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isNotInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + environment.getClass().getInterfaces()))); + }); + } - private void testAutowiredComponent(Class type, BiConsumer assertions) { - testAutowiredComponent(type, new RootBeanDefinition(type), assertions); - } + @Test + void processAheadOfTimeWhenHasLazyAutowiringOnFactoryMethod() { + RootBeanDefinition bd = new RootBeanDefinition(LazyFactoryMethodArgumentComponent.class); + bd.setFactoryMethodName("of"); + testAutowiredComponent(LazyFactoryMethodArgumentComponent.class, bd, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isNotInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + environment.getClass().getInterfaces()))); + }); + } + + private void testAutowiredComponent(Class type, BiConsumer assertions) { + testAutowiredComponent(type, new RootBeanDefinition(type), assertions); + } + + private void testAutowiredComponent(Class type, RootBeanDefinition beanDefinition, + BiConsumer assertions) { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.getDefaultListableBeanFactory().setAutowireCandidateResolver( + new ContextAnnotationAutowireCandidateResolver()); + registerBeanPostProcessor(applicationContext, + 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); + }); + } - private void testAutowiredComponent(Class type, RootBeanDefinition beanDefinition, - BiConsumer assertions) { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.getDefaultListableBeanFactory().setAutowireCandidateResolver( - new ContextAnnotationAutowireCandidateResolver()); - applicationContext.registerBeanDefinition(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, - BeanDefinitionBuilder - .rootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); - 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); - }); } - @Test - void processAheadOfTimeWhenHasInitDestroyMethods() { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.registerBeanDefinition( - AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, - BeanDefinitionBuilder - .rootBeanDefinition(CommonAnnotationBeanPostProcessor.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); - applicationContext.registerBeanDefinition("initDestroyComponent", - new RootBeanDefinition(InitDestroyComponent.class)); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent"); - InitDestroyComponent bean = freshApplicationContext.getBean(InitDestroyComponent.class); - assertThat(bean.events).containsExactly("init"); - freshApplicationContext.close(); - assertThat(bean.events).containsExactly("init", "destroy"); - }); + @Nested + class ResourceAutowiring { + + @Test + void processAheadOfTimeWhenHasResourceAutowiring() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + registerBeanPostProcessor(applicationContext, + AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, CommonAnnotationBeanPostProcessor.class); + registerStringBean(applicationContext, "text", "hello"); + 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"); + ResourceComponent bean = freshApplicationContext.getBean(ResourceComponent.class); + assertThat(bean.getText()).isEqualTo("hello"); + assertThat(bean.getCounter()).isEqualTo(42); + }); + } + + @Test + void processAheadOfTimeWhenHasLazyResourceAutowiringOnField() { + testResourceAutowiringComponent(LazyResourceFieldComponent.class, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isNotInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + environment.getClass().getInterfaces()))); + + }); + } + + @Test + void processAheadOfTimeWhenHasLazyResourceAutowiringOnMethod() { + testResourceAutowiringComponent(LazyResourceMethodComponent.class, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isNotInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(Environment.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + resourceLoader.getClass().getInterfaces()))); + }); + } + + private void testResourceAutowiringComponent(Class type, BiConsumer assertions) { + testResourceAutowiringComponent(type, new RootBeanDefinition(type), assertions); + } + + private void testResourceAutowiringComponent(Class type, RootBeanDefinition beanDefinition, + BiConsumer assertions) { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.getDefaultListableBeanFactory().setAutowireCandidateResolver( + new ContextAnnotationAutowireCandidateResolver()); + registerBeanPostProcessor(applicationContext, + 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"); + assertions.accept(freshApplicationContext.getBean("testComponent", type), generationContext); + }); + } } - @Test - void processAheadOfTimeWhenHasMultipleInitDestroyMethods() { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.registerBeanDefinition( - AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, - BeanDefinitionBuilder - .rootBeanDefinition(CommonAnnotationBeanPostProcessor.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); - RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyComponent.class); - beanDefinition.setInitMethodName("customInit"); - beanDefinition.setDestroyMethodName("customDestroy"); - applicationContext.registerBeanDefinition("initDestroyComponent", beanDefinition); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent"); - InitDestroyComponent bean = freshApplicationContext.getBean(InitDestroyComponent.class); - assertThat(bean.events).containsExactly("init", "customInit"); - freshApplicationContext.close(); - assertThat(bean.events).containsExactly("init", "customInit", "destroy", "customDestroy"); - }); + @Nested + class InitDestroy { + + @Test + void processAheadOfTimeWhenHasInitDestroyMethods() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + registerBeanPostProcessor(applicationContext, + 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"); + InitDestroyComponent bean = freshApplicationContext.getBean(InitDestroyComponent.class); + assertThat(bean.events).containsExactly("init"); + freshApplicationContext.close(); + assertThat(bean.events).containsExactly("init", "destroy"); + }); + } + + @Test + void processAheadOfTimeWhenHasMultipleInitDestroyMethods() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + registerBeanPostProcessor(applicationContext, + AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, CommonAnnotationBeanPostProcessor.class); + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyComponent.class); + beanDefinition.setInitMethodName("customInit"); + beanDefinition.setDestroyMethodName("customDestroy"); + applicationContext.registerBeanDefinition("initDestroyComponent", beanDefinition); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent"); + InitDestroyComponent bean = freshApplicationContext.getBean(InitDestroyComponent.class); + assertThat(bean.events).containsExactly("init", "customInit"); + freshApplicationContext.close(); + assertThat(bean.events).containsExactly("init", "customInit", "destroy", "customDestroy"); + }); + } + } @Test @@ -406,6 +487,130 @@ void processAheadOfTimeWhenHasCglibProxyWithArgumentsRegisterIntrospectionHintsO } + @Nested + class ActiveProfile { + + @ParameterizedTest + @MethodSource("activeProfilesParameters") + void processAheadOfTimeWhenHasActiveProfiles(String[] aotProfiles, String[] runtimeProfiles, String[] expectedActiveProfiles) { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + if (aotProfiles.length != 0) { + applicationContext.getEnvironment().setActiveProfiles(aotProfiles); + } + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); + if (runtimeProfiles.length != 0) { + freshApplicationContext.getEnvironment().setActiveProfiles(runtimeProfiles); + } + initializer.initialize(freshApplicationContext); + freshApplicationContext.refresh(); + assertThat(freshApplicationContext.getEnvironment().getActiveProfiles()).containsExactly(expectedActiveProfiles); + }); + } + + static Stream activeProfilesParameters() { + return Stream.of(Arguments.of(new String[] { "aot", "prod" }, new String[] {}, new String[] { "aot", "prod" }), + Arguments.of(new String[] {}, new String[] { "aot", "prod" }, new String[] { "aot", "prod" }), + Arguments.of(new String[] { "aot" }, new String[] { "prod" }, new String[] { "prod", "aot" }), + 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())); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + Employee employee = freshApplicationContext.getBean(Employee.class); + assertThat(employee.getName()).isEqualTo("John Smith"); + assertThat(employee.getAge()).isEqualTo(42); + assertThat(employee.getCompany()).isEqualTo("Acme Widgets, Inc."); + assertThat(freshApplicationContext.getBean("petIndexed", Pet.class) + .getName()).isEqualTo("Fido"); + assertThat(freshApplicationContext.getBean("petGeneric", Pet.class) + .getName()).isEqualTo("Dofi"); + }); + } + + @Test + void processAheadOfTimeWhenHasTypedStringValueWithType() { + GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); + applicationContext + .load(new ClassPathResource("applicationContextAotGeneratorTests-values-types.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + Employee employee = freshApplicationContext.getBean(Employee.class); + assertThat(employee.getName()).isEqualTo("John Smith"); + assertThat(employee.getAge()).isEqualTo(42); + assertThat(employee.getCompany()).isEqualTo("Acme Widgets, Inc."); + assertThat(compiled.getSourceFile(".*Employee__BeanDefinitions")) + .contains("new TypedStringValue(\"42\", Integer.class"); + }); + } + + @Test + void processAheadOfTimeWhenHasTypedStringValueWithExpression() { + GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); + applicationContext + .load(new ClassPathResource("applicationContextAotGeneratorTests-values-expressions.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + Employee employee = freshApplicationContext.getBean(Employee.class); + assertThat(employee.getName()).isEqualTo("John Smith"); + assertThat(employee.getAge()).isEqualTo(42); + assertThat(employee.getCompany()).isEqualTo("Acme Widgets, Inc."); + assertThat(freshApplicationContext.getBean("pet", Pet.class) + .getName()).isEqualTo("Fido"); + }); + } + + @Test + void processAheadOfTimeWhenXmlHasBeanReferences() { + GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); + applicationContext + .load(new ClassPathResource("applicationContextAotGeneratorTests-references.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBean("petInnerBean", Pet.class) + .getName()).isEqualTo("Fido"); + assertThat(freshApplicationContext.getBean("petRefBean", Pet.class) + .getName()).isEqualTo("Dofi"); + }); + } + + } + + private static void registerBeanPostProcessor(GenericApplicationContext applicationContext, + String beanName, Class beanPostProcessorClass) { + + applicationContext.registerBeanDefinition(beanName, BeanDefinitionBuilder + .rootBeanDefinition(beanPostProcessorClass).setRole(BeanDefinition.ROLE_INFRASTRUCTURE) + .getBeanDefinition()); + } + + private static void registerStringBean(GenericApplicationContext applicationContext, + String beanName, String value) { + + applicationContext.registerBeanDefinition(beanName, BeanDefinitionBuilder + .rootBeanDefinition(String.class).addConstructorArgValue(value) + .getBeanDefinition()); + } + + private static void registerIntegerBean(GenericApplicationContext applicationContext, + String beanName, int value) { + + applicationContext.registerBeanDefinition(beanName, BeanDefinitionBuilder + .rootBeanDefinition(Integer.class, "valueOf").addConstructorArgValue(value) + .getBeanDefinition()); + } + private Consumer> doesNotHaveProxyFor(Class target) { return hints -> assertThat(hints).noneMatch(hint -> hint.getProxiedInterfaces().get(0).equals(TypeReference.of(target))); 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 661c4d3eb245..21e6db8d941a 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-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class ContextNamespaceHandlerTests { @AfterEach void tearDown() { - System.getProperties().remove("foo"); + System.clearProperty("foo"); } 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 d263fb1e8ce0..91a750956aa3 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ class AnnotationDrivenEventListenerTests { @AfterEach - public void closeContext() { + void closeContext() { if (this.context != null) { this.context.close(); } @@ -165,11 +165,27 @@ void contextEventsAreReceived() { assertThat(events).as("Wrong number of initial context events").hasSize(1); assertThat(events.get(0).getClass()).isEqualTo(ContextRefreshedEvent.class); + this.context.start(); + List eventsAfterStart = this.eventCollector.getEvents(listener); + assertThat(eventsAfterStart).as("Wrong number of context events on start").hasSize(2); + assertThat(eventsAfterStart.get(1).getClass()).isEqualTo(ContextStartedEvent.class); + this.eventCollector.assertTotalEventsCount(2); + this.context.stop(); List eventsAfterStop = this.eventCollector.getEvents(listener); - assertThat(eventsAfterStop).as("Wrong number of context events on shutdown").hasSize(2); - assertThat(eventsAfterStop.get(1).getClass()).isEqualTo(ContextStoppedEvent.class); - this.eventCollector.assertTotalEventsCount(2); + assertThat(eventsAfterStop).as("Wrong number of context events on stop").hasSize(3); + assertThat(eventsAfterStop.get(2).getClass()).isEqualTo(ContextStoppedEvent.class); + this.eventCollector.assertTotalEventsCount(3); + + this.context.close(); + List eventsAfterClose = this.eventCollector.getEvents(listener); + assertThat(eventsAfterClose).as("Wrong number of context events on close").hasSize(4); + assertThat(eventsAfterClose.get(3).getClass()).isEqualTo(ContextClosedEvent.class); + this.eventCollector.assertTotalEventsCount(4); + + // Further events are supposed to be ignored after context close + this.context.publishEvent(new ContextClosedEvent(this.context)); + this.eventCollector.assertTotalEventsCount(4); } @Test @@ -180,8 +196,7 @@ void methodSignatureNoEvent() { failingContext.register(BasicConfiguration.class, InvalidMethodSignatureEventListener.class); - assertThatExceptionOfType(BeanInitializationException.class).isThrownBy(() -> - failingContext.refresh()) + assertThatExceptionOfType(BeanInitializationException.class).isThrownBy(failingContext::refresh) .withMessageContaining(InvalidMethodSignatureEventListener.class.getName()) .withMessageContaining("cannotBeCalled"); } @@ -628,7 +643,7 @@ void orderedListeners() { load(OrderedTestListener.class); OrderedTestListener listener = this.context.getBean(OrderedTestListener.class); - assertThat(listener.order.isEmpty()).isTrue(); + assertThat(listener.order).isEmpty(); this.context.publishEvent("whatever"); assertThat(listener.order).contains("first", "second", "third"); } @@ -664,14 +679,14 @@ private void load(Class... classes) { List> allClasses = new ArrayList<>(); allClasses.add(BasicConfiguration.class); allClasses.addAll(Arrays.asList(classes)); - doLoad(allClasses.toArray(new Class[allClasses.size()])); + doLoad(allClasses.toArray(new Class[0])); } private void loadAsync(Class... classes) { List> allClasses = new ArrayList<>(); allClasses.add(AsyncConfiguration.class); allClasses.addAll(Arrays.asList(classes)); - doLoad(allClasses.toArray(new Class[allClasses.size()])); + doLoad(allClasses.toArray(new Class[0])); } private void doLoad(Class... classes) { @@ -713,7 +728,7 @@ public boolean valid(Double ratio) { } - static abstract class AbstractTestEventListener extends AbstractIdentifiable { + abstract static class AbstractTestEventListener extends AbstractIdentifiable { @Autowired private EventCollector eventCollector; 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 ea7e55cb4c03..cc26a34e560e 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-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,12 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.BeansException; @@ -39,6 +42,11 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.event.test.self_inject.MyAspect; +import org.springframework.context.event.test.self_inject.MyEventListener; +import org.springframework.context.event.test.self_inject.MyEventPublisher; +import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticMessageSource; @@ -54,10 +62,12 @@ import static org.assertj.core.api.Assertions.assertThatRuntimeException; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.springframework.context.support.AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME; /** @@ -68,10 +78,10 @@ * @author Stephane Nicoll * @author Juergen Hoeller */ -public class ApplicationContextEventTests extends AbstractApplicationEventListenerTests { +class ApplicationContextEventTests extends AbstractApplicationEventListenerTests { @Test - public void multicastSimpleEvent() { + void multicastSimpleEvent() { multicastEvent(true, ApplicationListener.class, new ContextRefreshedEvent(new StaticApplicationContext()), null); multicastEvent(true, ApplicationListener.class, @@ -79,40 +89,40 @@ public void multicastSimpleEvent() { } @Test - public void multicastGenericEvent() { + void multicastGenericEvent() { multicastEvent(true, StringEventListener.class, createGenericTestEvent("test"), ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); } @Test - public void multicastGenericEventWrongType() { + void multicastGenericEventWrongType() { multicastEvent(false, StringEventListener.class, createGenericTestEvent(123L), ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); } @Test - public void multicastGenericEventWildcardSubType() { + void multicastGenericEventWildcardSubType() { multicastEvent(false, StringEventListener.class, createGenericTestEvent("test"), getGenericApplicationEventType("wildcardEvent")); } @Test - public void multicastConcreteTypeGenericListener() { + void multicastConcreteTypeGenericListener() { multicastEvent(true, StringEventListener.class, new StringEvent(this, "test"), null); } @Test - public void multicastConcreteWrongTypeGenericListener() { + void multicastConcreteWrongTypeGenericListener() { multicastEvent(false, StringEventListener.class, new LongEvent(this, 123L), null); } @Test - public void multicastSmartGenericTypeGenericListener() { + void multicastSmartGenericTypeGenericListener() { multicastEvent(true, StringEventListener.class, new SmartGenericTestEvent<>(this, "test"), null); } @Test - public void multicastSmartGenericWrongTypeGenericListener() { + void multicastSmartGenericWrongTypeGenericListener() { multicastEvent(false, StringEventListener.class, new SmartGenericTestEvent<>(this, 123L), null); } @@ -129,29 +139,54 @@ private void multicastEvent(boolean match, Class listenerType, ApplicationEve else { smc.multicastEvent(event); } - int invocation = match ? 1 : 0; + int invocation = (match ? 1 : 0); verify(listener, times(invocation)).onApplicationEvent(event); } @Test - public void simpleApplicationEventMulticasterWithTaskExecutor() { + void simpleApplicationEventMulticasterWithTaskExecutor() { @SuppressWarnings("unchecked") ApplicationListener listener = mock(); + willReturn(true).given(listener).supportsAsyncExecution(); ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + AtomicBoolean invoked = new AtomicBoolean(); smc.setTaskExecutor(command -> { + invoked.set(true); command.run(); command.run(); }); smc.addApplicationListener(listener); smc.multicastEvent(evt); + assertThat(invoked.get()).isTrue(); verify(listener, times(2)).onApplicationEvent(evt); } @Test - public void simpleApplicationEventMulticasterWithException() { + void simpleApplicationEventMulticasterWithTaskExecutorAndNonAsyncListener() { + @SuppressWarnings("unchecked") + ApplicationListener listener = mock(); + willReturn(false).given(listener).supportsAsyncExecution(); + ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + AtomicBoolean invoked = new AtomicBoolean(); + smc.setTaskExecutor(command -> { + invoked.set(true); + command.run(); + command.run(); + }); + smc.addApplicationListener(listener); + + smc.multicastEvent(evt); + assertThat(invoked.get()).isFalse(); + verify(listener, times(1)).onApplicationEvent(evt); + } + + @Test + void simpleApplicationEventMulticasterWithException() { @SuppressWarnings("unchecked") ApplicationListener listener = mock(); ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); @@ -167,7 +202,7 @@ public void simpleApplicationEventMulticasterWithException() { } @Test - public void simpleApplicationEventMulticasterWithErrorHandler() { + void simpleApplicationEventMulticasterWithErrorHandler() { @SuppressWarnings("unchecked") ApplicationListener listener = mock(); ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); @@ -181,7 +216,7 @@ public void simpleApplicationEventMulticasterWithErrorHandler() { } @Test - public void orderedListeners() { + void orderedListeners() { MyOrderedListener1 listener1 = new MyOrderedListener1(); MyOrderedListener2 listener2 = new MyOrderedListener2(listener1); @@ -195,7 +230,7 @@ public void orderedListeners() { } @Test - public void orderedListenersWithAnnotation() { + void orderedListenersWithAnnotation() { MyOrderedListener3 listener1 = new MyOrderedListener3(); MyOrderedListener4 listener2 = new MyOrderedListener4(listener1); @@ -244,8 +279,26 @@ public void proxiedListenersMixedWithTargetListeners() { assertThat(listener1.seenEvents).hasSize(2); } + /** + * Regression test for issue 28283, + * where event listeners proxied due to e.g. + *
    + *
  • {@code @Transactional} annotations in their methods or
  • + *
  • being targeted by aspects
  • + *
+ * were added to the list of application listener beans twice (both proxy and unwrapped target). + */ @Test - public void testEventPublicationInterceptor() throws Throwable { + void eventForSelfInjectedProxiedListenerFiredOnlyOnce() { + AbstractApplicationContext context = new AnnotationConfigApplicationContext( + MyAspect.class, MyEventListener.class, MyEventPublisher.class); + context.getBean(MyEventPublisher.class).publishMyEvent("hello"); + assertThat(context.getBean(MyEventListener.class).eventCount).isEqualTo(1); + context.close(); + } + + @Test + void testEventPublicationInterceptor() throws Throwable { MethodInvocation invocation = mock(); ApplicationContext ctx = mock(); @@ -261,7 +314,7 @@ public void testEventPublicationInterceptor() throws Throwable { } @Test - public void listenersInApplicationContext() { + void listenersInApplicationContext() { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("listener1", new RootBeanDefinition(MyOrderedListener1.class)); RootBeanDefinition listener2 = new RootBeanDefinition(MyOrderedListener2.class); @@ -298,7 +351,7 @@ public void listenersInApplicationContext() { } @Test - public void listenersInApplicationContextWithPayloadEvents() { + void listenersInApplicationContextWithPayloadEvents() { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("listener", new RootBeanDefinition(MyPayloadListener.class)); context.refresh(); @@ -317,7 +370,7 @@ public void listenersInApplicationContextWithPayloadEvents() { } @Test - public void listenersInApplicationContextWithNestedChild() { + void listenersInApplicationContextWithNestedChild() { StaticApplicationContext context = new StaticApplicationContext(); RootBeanDefinition nestedChild = new RootBeanDefinition(StaticApplicationContext.class); nestedChild.getPropertyValues().add("parent", context); @@ -341,7 +394,7 @@ public void listenersInApplicationContextWithNestedChild() { } @Test - public void nonSingletonListenerInApplicationContext() { + void nonSingletonListenerInApplicationContext() { StaticApplicationContext context = new StaticApplicationContext(); RootBeanDefinition listener = new RootBeanDefinition(MyNonSingletonListener.class); listener.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -373,7 +426,7 @@ public void nonSingletonListenerInApplicationContext() { } @Test - public void listenerAndBroadcasterWithCircularReference() { + void listenerAndBroadcasterWithCircularReference() { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("broadcaster", new RootBeanDefinition(BeanThatBroadcasts.class)); RootBeanDefinition listenerDef = new RootBeanDefinition(BeanThatListens.class); @@ -389,7 +442,7 @@ public void listenerAndBroadcasterWithCircularReference() { } @Test - public void innerBeanAsListener() { + void innerBeanAsListener() { StaticApplicationContext context = new StaticApplicationContext(); RootBeanDefinition listenerDef = new RootBeanDefinition(TestBean.class); listenerDef.getPropertyValues().add("friends", new RootBeanDefinition(BeanThatListens.class)); @@ -405,7 +458,7 @@ public void innerBeanAsListener() { } @Test - public void anonymousClassAsListener() { + void anonymousClassAsListener() { final Set seenEvents = new HashSet<>(); StaticApplicationContext context = new StaticApplicationContext(); context.addApplicationListener((MyEvent event) -> seenEvents.add(event)); @@ -422,7 +475,7 @@ public void anonymousClassAsListener() { } @Test - public void lambdaAsListener() { + void lambdaAsListener() { final Set seenEvents = new HashSet<>(); StaticApplicationContext context = new StaticApplicationContext(); ApplicationListener listener = seenEvents::add; @@ -440,7 +493,7 @@ public void lambdaAsListener() { } @Test - public void lambdaAsListenerWithErrorHandler() { + void lambdaAsListenerWithErrorHandler() { final Set seenEvents = new HashSet<>(); StaticApplicationContext context = new StaticApplicationContext(); SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster(); @@ -461,7 +514,7 @@ public void lambdaAsListenerWithErrorHandler() { } @Test - public void lambdaAsListenerWithJava8StyleClassCastMessage() { + void lambdaAsListenerWithJava8StyleClassCastMessage() { StaticApplicationContext context = new StaticApplicationContext(); ApplicationListener listener = event -> { throw new ClassCastException(event.getClass().getName()); }; @@ -473,7 +526,7 @@ public void lambdaAsListenerWithJava8StyleClassCastMessage() { } @Test - public void lambdaAsListenerWithJava9StyleClassCastMessage() { + void lambdaAsListenerWithJava9StyleClassCastMessage() { StaticApplicationContext context = new StaticApplicationContext(); ApplicationListener listener = event -> { throw new ClassCastException("spring.context/" + event.getClass().getName()); }; @@ -485,7 +538,21 @@ public void lambdaAsListenerWithJava9StyleClassCastMessage() { } @Test - public void beanPostProcessorPublishesEvents() { + @SuppressWarnings("unchecked") + void addListenerWithConsumer() { + Consumer consumer = mock(Consumer.class); + GenericApplicationContext context = new GenericApplicationContext(); + context.addApplicationListener(GenericApplicationListener.forEventType( + ContextRefreshedEvent.class, consumer)); + context.refresh(); + ArgumentCaptor captor = ArgumentCaptor.forClass(ContextRefreshedEvent.class); + verify(consumer).accept(captor.capture()); + assertThat(captor.getValue().getApplicationContext()).isSameAs(context); + verifyNoMoreInteractions(consumer); + } + + @Test + void beanPostProcessorPublishesEvents() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); @@ -500,7 +567,7 @@ public void beanPostProcessorPublishesEvents() { } @Test - public void initMethodPublishesEvent() { + void initMethodPublishesEvent() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); @@ -515,7 +582,7 @@ public void initMethodPublishesEvent() { } @Test - public void initMethodPublishesAsyncEvent() { + void initMethodPublishesAsyncEvent() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); @@ -568,7 +635,7 @@ public interface MyOrderedListenerIfc extends Applic } - public static abstract class MyOrderedListenerBase implements MyOrderedListenerIfc { + public abstract static class MyOrderedListenerBase implements MyOrderedListenerIfc { @Override public int getOrder() { @@ -587,7 +654,7 @@ public MyOrderedListener2(MyOrderedListener1 otherListener) { @Override public void onApplicationEvent(MyEvent event) { - assertThat(this.otherListener.seenEvents.contains(event)).isTrue(); + assertThat(this.otherListener.seenEvents).contains(event); } } @@ -639,7 +706,7 @@ public MyOrderedListener4(MyOrderedListener3 otherListener) { @Override public void onApplicationEvent(MyEvent event) { - assertThat(this.otherListener.seenEvents.contains(event)).isTrue(); + assertThat(this.otherListener.seenEvents).contains(event); } } @@ -676,7 +743,7 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.publisher.publishEvent(new MyEvent(this)); } } diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java index f8c4e331fa8b..fbafc8e0f207 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableTypeProvider; import org.springframework.core.annotation.Order; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -50,7 +51,7 @@ * @author Juergen Hoeller * @author Simon Baslé */ -public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEventListenerTests { +class ApplicationListenerMethodAdapterTests extends AbstractApplicationEventListenerTests { private final SampleEvents sampleEvents = spy(new SampleEvents()); @@ -58,89 +59,89 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv @Test - public void rawListener() { + void rawListener() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleRaw", ApplicationEvent.class); supportsEventType(true, method, ResolvableType.forClass(ApplicationEvent.class)); } @Test - public void rawListenerWithGenericEvent() { + void rawListenerWithGenericEvent() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleRaw", ApplicationEvent.class); supportsEventType(true, method, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); } @Test - public void genericListener() { + void genericListener() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericString", GenericTestEvent.class); supportsEventType(true, method, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); } @Test - public void genericListenerWrongParameterizedType() { + void genericListenerWrongParameterizedType() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericString", GenericTestEvent.class); supportsEventType(false, method, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); } @Test - public void genericListenerWithUnresolvedGenerics() { + void genericListenerWithUnresolvedGenerics() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericString", GenericTestEvent.class); supportsEventType(true, method, ResolvableType.forClass(GenericTestEvent.class)); } @Test - public void listenerWithPayloadAndGenericInformation() { + void listenerWithPayloadAndGenericInformation() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); supportsEventType(true, method, createPayloadEventType(String.class)); } @Test - public void listenerWithInvalidPayloadAndGenericInformation() { + void listenerWithInvalidPayloadAndGenericInformation() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); supportsEventType(false, method, createPayloadEventType(Integer.class)); } @Test - public void listenerWithPayloadTypeErasure() { // Always accept such event when the type is unknown + void listenerWithPayloadTypeErasure() { // Always accept such event when the type is unknown Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); supportsEventType(true, method, ResolvableType.forClass(PayloadApplicationEvent.class)); } @Test - public void listenerWithSubTypeSeveralGenerics() { + void listenerWithSubTypeSeveralGenerics() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); supportsEventType(true, method, ResolvableType.forClass(PayloadTestEvent.class)); } @Test - public void listenerWithSubTypeSeveralGenericsResolved() { + void listenerWithSubTypeSeveralGenericsResolved() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); supportsEventType(true, method, ResolvableType.forClass(PayloadStringTestEvent.class)); } @Test - public void listenerWithAnnotationValue() { + void listenerWithAnnotationValue() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationValue"); supportsEventType(true, method, createPayloadEventType(String.class)); } @Test - public void listenerWithAnnotationClasses() { + void listenerWithAnnotationClasses() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationClasses"); supportsEventType(true, method, createPayloadEventType(String.class)); } @Test - public void listenerWithAnnotationValueAndParameter() { + void listenerWithAnnotationValueAndParameter() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleStringAnnotationValueAndParameter", String.class); supportsEventType(true, method, createPayloadEventType(String.class)); } @Test - public void listenerWithSeveralTypes() { + void listenerWithSeveralTypes() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringOrInteger"); supportsEventType(true, method, createPayloadEventType(String.class)); supportsEventType(true, method, createPayloadEventType(Integer.class)); @@ -148,27 +149,27 @@ public void listenerWithSeveralTypes() { } @Test - public void listenerWithTooManyParameters() { + void listenerWithTooManyParameters() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "tooManyParameters", String.class, String.class); assertThatIllegalStateException().isThrownBy(() -> createTestInstance(method)); } @Test - public void listenerWithNoParameter() { + void listenerWithNoParameter() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "noParameter"); assertThatIllegalStateException().isThrownBy(() -> createTestInstance(method)); } @Test - public void listenerWithMoreThanOneParameter() { + void listenerWithMoreThanOneParameter() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "moreThanOneParameter", String.class, Integer.class); assertThatIllegalStateException().isThrownBy(() -> createTestInstance(method)); } @Test - public void defaultOrder() { + void defaultOrder() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericString", GenericTestEvent.class); ApplicationListenerMethodAdapter adapter = createTestInstance(method); @@ -176,7 +177,7 @@ public void defaultOrder() { } @Test - public void specifiedOrder() { + void specifiedOrder() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleRaw", ApplicationEvent.class); ApplicationListenerMethodAdapter adapter = createTestInstance(method); @@ -184,7 +185,7 @@ public void specifiedOrder() { } @Test - public void invokeListener() { + void invokeListener() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericString", GenericTestEvent.class); GenericTestEvent event = createGenericTestEvent("test"); @@ -193,7 +194,7 @@ public void invokeListener() { } @Test - public void invokeListenerWithGenericEvent() { + void invokeListenerWithGenericEvent() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericString", GenericTestEvent.class); GenericTestEvent event = new SmartGenericTestEvent<>(this, "test"); @@ -202,7 +203,7 @@ public void invokeListenerWithGenericEvent() { } @Test - public void invokeListenerWithGenericPayload() { + void invokeListenerWithGenericPayload() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class); EntityWrapper payload = new EntityWrapper<>("test"); @@ -211,7 +212,7 @@ public void invokeListenerWithGenericPayload() { } @Test - public void invokeListenerWithWrongGenericPayload() { + void invokeListenerWithWrongGenericPayload() { Method method = ReflectionUtils.findMethod (SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class); EntityWrapper payload = new EntityWrapper<>(123); @@ -220,7 +221,7 @@ public void invokeListenerWithWrongGenericPayload() { } @Test - public void invokeListenerWithAnyGenericPayload() { + void invokeListenerWithAnyGenericPayload() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericAnyPayload", EntityWrapper.class); EntityWrapper payload = new EntityWrapper<>("test"); @@ -229,7 +230,7 @@ public void invokeListenerWithAnyGenericPayload() { } @Test - public void invokeListenerRuntimeException() { + void invokeListenerRuntimeException() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "generateRuntimeException", GenericTestEvent.class); GenericTestEvent event = createGenericTestEvent("fail"); @@ -241,7 +242,7 @@ public void invokeListenerRuntimeException() { } @Test - public void invokeListenerCheckedException() { + void invokeListenerCheckedException() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "generateCheckedException", GenericTestEvent.class); GenericTestEvent event = createGenericTestEvent("fail"); @@ -252,7 +253,7 @@ public void invokeListenerCheckedException() { } @Test - public void invokeListenerInvalidProxy() { + void invokeListenerInvalidProxy() { Object target = new InvalidProxyTestBean(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTarget(target); @@ -269,7 +270,7 @@ public void invokeListenerInvalidProxy() { } @Test - public void invokeListenerWithPayload() { + void invokeListenerWithPayload() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); invokeListener(method, event); @@ -277,7 +278,7 @@ public void invokeListenerWithPayload() { } @Test - public void invokeListenerWithPayloadWrongType() { + void invokeListenerWithPayloadWrongType() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, 123L); invokeListener(method, event); @@ -285,7 +286,7 @@ public void invokeListenerWithPayloadWrongType() { } @Test - public void invokeListenerWithAnnotationValue() { + void invokeListenerWithAnnotationValue() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationClasses"); PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); invokeListener(method, event); @@ -293,7 +294,7 @@ public void invokeListenerWithAnnotationValue() { } @Test - public void invokeListenerWithAnnotationValueAndParameter() { + void invokeListenerWithAnnotationValueAndParameter() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleStringAnnotationValueAndParameter", String.class); PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); @@ -302,7 +303,7 @@ public void invokeListenerWithAnnotationValueAndParameter() { } @Test - public void invokeListenerWithSeveralTypes() { + void invokeListenerWithSeveralTypes() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringOrInteger"); PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); invokeListener(method, event); @@ -316,13 +317,13 @@ public void invokeListenerWithSeveralTypes() { } @Test - public void beanInstanceRetrievedAtEveryInvocation() { + void beanInstanceRetrievedAtEveryInvocation() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleGenericString", GenericTestEvent.class); given(this.context.getBean("testBean")).willReturn(this.sampleEvents); ApplicationListenerMethodAdapter listener = new ApplicationListenerMethodAdapter( "testBean", GenericTestEvent.class, method); - listener.init(this.context, new EventExpressionEvaluator()); + listener.init(this.context, new EventExpressionEvaluator(new StandardEvaluationContext())); GenericTestEvent event = createGenericTestEvent("test"); diff --git a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java index 6fdb68bb0af6..fe0dc36be829 100644 --- a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ /** * @author Stephane Nicoll */ -public class GenericApplicationListenerAdapterTests extends AbstractApplicationEventListenerTests { +class GenericApplicationListenerAdapterTests extends AbstractApplicationEventListenerTests { @Test - public void supportsEventTypeWithSmartApplicationListener() { + void supportsEventTypeWithSmartApplicationListener() { SmartApplicationListener smartListener = mock(); GenericApplicationListenerAdapter listener = new GenericApplicationListenerAdapter(smartListener); ResolvableType type = ResolvableType.forClass(ApplicationEvent.class); @@ -44,7 +44,7 @@ public void supportsEventTypeWithSmartApplicationListener() { } @Test - public void supportsSourceTypeWithSmartApplicationListener() { + void supportsSourceTypeWithSmartApplicationListener() { SmartApplicationListener smartListener = mock(); GenericApplicationListenerAdapter listener = new GenericApplicationListenerAdapter(smartListener); listener.supportsSourceType(Object.class); @@ -52,7 +52,7 @@ public void supportsSourceTypeWithSmartApplicationListener() { } @Test - public void genericListenerStrictType() { + void genericListenerStrictType() { supportsEventType(true, StringEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); } @@ -84,44 +84,44 @@ public void genericListenerStrictTypeEventSubType() { } @Test - public void genericListenerStrictTypeNotMatching() { + void genericListenerStrictTypeNotMatching() { supportsEventType(false, StringEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); } @Test - public void genericListenerStrictTypeEventSubTypeNotMatching() { + void genericListenerStrictTypeEventSubTypeNotMatching() { LongEvent stringEvent = new LongEvent(this, 123L); ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); supportsEventType(false, StringEventListener.class, eventType); } @Test - public void genericListenerStrictTypeNotMatchTypeErasure() { + void genericListenerStrictTypeNotMatchTypeErasure() { GenericTestEvent longEvent = createGenericTestEvent(123L); ResolvableType eventType = ResolvableType.forType(longEvent.getClass()); supportsEventType(false, StringEventListener.class, eventType); } @Test - public void genericListenerStrictTypeSubClass() { + void genericListenerStrictTypeSubClass() { supportsEventType(false, ObjectEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); } @Test - public void genericListenerUpperBoundType() { + void genericListenerUpperBoundType() { supportsEventType(true, UpperBoundEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, IllegalStateException.class)); } @Test - public void genericListenerUpperBoundTypeNotMatching() { + void genericListenerUpperBoundTypeNotMatching() { supportsEventType(false, UpperBoundEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, IOException.class)); } @Test - public void genericListenerWildcardType() { + void genericListenerWildcardType() { supportsEventType(true, GenericEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); } @@ -134,7 +134,7 @@ public void genericListenerWildcardTypeTypeErasure() { } @Test - public void genericListenerRawType() { + void genericListenerRawType() { supportsEventType(true, RawApplicationListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); } diff --git a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerTests.java new file mode 100644 index 000000000000..d1d23a2d45c2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GenericApplicationListener}. + * + * @author Stephane Nicoll + */ +class GenericApplicationListenerTests extends AbstractApplicationEventListenerTests { + + @Test + void forEventTypeWithStrictTypeMatching() { + GenericApplicationListener listener = GenericApplicationListener + .forEventType(StringEvent.class, event -> {}); + assertThat(listener.supportsEventType(ResolvableType.forClass(StringEvent.class))).isTrue(); + } + + @Test + void forEventTypeWithSubClass() { + GenericApplicationListener listener = GenericApplicationListener + .forEventType(GenericTestEvent.class, event -> {}); + assertThat(listener.supportsEventType(ResolvableType.forClass(StringEvent.class))).isTrue(); + } + + @Test + void forEventTypeWithSuperClass() { + GenericApplicationListener listener = GenericApplicationListener + .forEventType(StringEvent.class, event -> {}); + assertThat(listener.supportsEventType(ResolvableType.forClass(GenericTestEvent.class))).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void forEventTypeInvokesConsumer() { + Consumer consumer = mock(Consumer.class); + GenericApplicationListener listener = GenericApplicationListener + .forEventType(StringEvent.class, consumer); + StringEvent event = new StringEvent(this, "one"); + StringEvent event2 = new StringEvent(this, "two"); + listener.onApplicationEvent(event); + listener.onApplicationEvent(event2); + InOrder ordered = inOrder(consumer); + ordered.verify(consumer).accept(event); + ordered.verify(consumer).accept(event2); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/LifecycleEventTests.java b/spring-context/src/test/java/org/springframework/context/event/LifecycleEventTests.java index 5c4917c7fdab..a3e4bc820b21 100644 --- a/spring-context/src/test/java/org/springframework/context/event/LifecycleEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/LifecycleEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ * @author Mark Fisher * @author Juergen Hoeller */ -public class LifecycleEventTests { +class LifecycleEventTests { @Test - public void contextStartedEvent() { + void contextStartedEvent() { StaticApplicationContext context = new StaticApplicationContext(); context.registerSingleton("lifecycle", LifecycleTestBean.class); context.registerSingleton("listener", LifecycleListener.class); @@ -49,7 +49,7 @@ public void contextStartedEvent() { } @Test - public void contextStoppedEvent() { + void contextStoppedEvent() { StaticApplicationContext context = new StaticApplicationContext(); context.registerSingleton("lifecycle", LifecycleTestBean.class); context.registerSingleton("listener", LifecycleListener.class); diff --git a/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java index 4bb408101b57..8c437c6d8507 100644 --- a/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,7 @@ void testEventClassWithPayloadType() { PayloadApplicationEvent> event = new PayloadApplicationEvent<>(this, new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); ac.publishEvent(event); - assertThat(ac.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue(); + assertThat(ac.getBean(NumberHolderListener.class).events).contains(event.getPayload()); ac.close(); } @@ -91,7 +91,7 @@ void testEventClassWithPayloadTypeOnParentContext() { PayloadApplicationEvent> event = new PayloadApplicationEvent<>(this, new NumberHolder<>(42), ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); ac.publishEvent(event); - assertThat(parent.getBean(NumberHolderListener.class).events.contains(event.getPayload())).isTrue(); + assertThat(parent.getBean(NumberHolderListener.class).events).contains(event.getPayload()); ac.close(); parent.close(); } @@ -99,7 +99,7 @@ void testEventClassWithPayloadTypeOnParentContext() { @Test @SuppressWarnings("resource") void testPayloadObjectWithPayloadType() { - final Object payload = new NumberHolder<>(42); + final NumberHolder payload = new NumberHolder<>(42); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(NumberHolderListener.class) { @Override @@ -110,14 +110,14 @@ protected void finishRefresh() throws BeansException { } }; - assertThat(ac.getBean(NumberHolderListener.class).events.contains(payload)).isTrue(); + assertThat(ac.getBean(NumberHolderListener.class).events).contains(payload); ac.close(); } @Test @SuppressWarnings("resource") void testPayloadObjectWithPayloadTypeOnParentContext() { - final Object payload = new NumberHolder<>(42); + final NumberHolder payload = new NumberHolder<>(42); ConfigurableApplicationContext parent = new AnnotationConfigApplicationContext(NumberHolderListener.class); ConfigurableApplicationContext ac = new GenericApplicationContext(parent) { @@ -130,7 +130,7 @@ protected void finishRefresh() throws BeansException { }; ac.refresh(); - assertThat(parent.getBean(NumberHolderListener.class).events.contains(payload)).isTrue(); + assertThat(parent.getBean(NumberHolderListener.class).events).contains(payload); ac.close(); parent.close(); } @@ -142,7 +142,7 @@ void testEventClassWithInterface() { AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); ac.publishEvent(event); - assertThat(ac.getBean(AuditableListener.class).events.contains(event)).isTrue(); + assertThat(ac.getBean(AuditableListener.class).events).contains(event); ac.close(); } @@ -155,7 +155,7 @@ void testEventClassWithInterfaceOnParentContext() { AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); ac.publishEvent(event); - assertThat(parent.getBean(AuditableListener.class).events.contains(event)).isTrue(); + assertThat(parent.getBean(AuditableListener.class).events).contains(event); ac.close(); parent.close(); } @@ -165,7 +165,7 @@ void testEventClassWithInterfaceOnParentContext() { void testProgrammaticEventListener() { List events = new ArrayList<>(); ApplicationListener> listener = events::add; - ApplicationListener> mismatch = (event -> event.getPayload()); + ApplicationListener> mismatch = (PayloadApplicationEvent::getPayload); ConfigurableApplicationContext ac = new GenericApplicationContext(); ac.addApplicationListener(listener); @@ -174,7 +174,7 @@ void testProgrammaticEventListener() { AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); ac.publishEvent(event); - assertThat(events.contains(event)).isTrue(); + assertThat(events).contains(event); ac.close(); } @@ -183,7 +183,7 @@ void testProgrammaticEventListener() { void testProgrammaticEventListenerOnParentContext() { List events = new ArrayList<>(); ApplicationListener> listener = events::add; - ApplicationListener> mismatch = (event -> event.getPayload()); + ApplicationListener> mismatch = (PayloadApplicationEvent::getPayload); ConfigurableApplicationContext parent = new GenericApplicationContext(); parent.addApplicationListener(listener); @@ -194,7 +194,7 @@ void testProgrammaticEventListenerOnParentContext() { AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); ac.publishEvent(event); - assertThat(events.contains(event)).isTrue(); + assertThat(events).contains(event); ac.close(); parent.close(); } @@ -213,7 +213,7 @@ void testProgrammaticPayloadListener() { String payload = "xyz"; ac.publishEvent(payload); - assertThat(events.contains(payload)).isTrue(); + assertThat(events).contains(payload); ac.close(); } @@ -233,7 +233,7 @@ void testProgrammaticPayloadListenerOnParentContext() { String payload = "xyz"; ac.publishEvent(payload); - assertThat(events.contains(payload)).isTrue(); + assertThat(events).contains(payload); ac.close(); parent.close(); } @@ -245,7 +245,7 @@ void testPlainPayloadListener() { String payload = "xyz"; ac.publishEvent(payload); - assertThat(ac.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue(); + assertThat(ac.getBean(PlainPayloadListener.class).events).contains(payload); ac.close(); } @@ -258,7 +258,7 @@ void testPlainPayloadListenerOnParentContext() { String payload = "xyz"; ac.publishEvent(payload); - assertThat(parent.getBean(PlainPayloadListener.class).events.contains(payload)).isTrue(); + assertThat(parent.getBean(PlainPayloadListener.class).events).contains(payload); ac.close(); parent.close(); } diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyAspect.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyAspect.java new file mode 100644 index 000000000000..45c25a9a00ee --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyAspect.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test.self_inject; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class MyAspect { + + @Before("within(org.springframework.context.event.test.self_inject.MyEventListener)") + public void myAdvice(JoinPoint joinPoint) { + // System.out.println(joinPoint); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEvent.java new file mode 100644 index 000000000000..bd47102348f6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test.self_inject; + +import org.springframework.context.ApplicationEvent; + +@SuppressWarnings("serial") +public class MyEvent extends ApplicationEvent { + + @SuppressWarnings("unused") + private String message; + + public MyEvent(Object source, String message) { + super(source); + this.message = message; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventListener.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventListener.java new file mode 100644 index 000000000000..fb15ad17446e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test.self_inject; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Component +public class MyEventListener implements ApplicationListener { + + public int eventCount; + + @SuppressWarnings("unused") + @Autowired + private MyEventListener eventDemoListener; + + @Override + public void onApplicationEvent(MyEvent event) { + eventCount++; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventPublisher.java b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventPublisher.java new file mode 100644 index 000000000000..9063b247edb1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/self_inject/MyEventPublisher.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test.self_inject; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +public class MyEventPublisher { + + @Autowired + private ApplicationEventPublisher eventPublisher; + + public void publishMyEvent(String message) { + eventPublisher.publishEvent(new MyEvent(this, message)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/AnnotatedElementKeyTests.java b/spring-context/src/test/java/org/springframework/context/expression/AnnotatedElementKeyTests.java index 6726f65a6b12..897e36890a89 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/AnnotatedElementKeyTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/AnnotatedElementKeyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ void noTargetClassNotEquals() { AnnotatedElementKey first = new AnnotatedElementKey(this.method, getClass()); AnnotatedElementKey second = new AnnotatedElementKey(this.method, null); - assertThat(first.equals(second)).isFalse(); + assertThat(first).isNotEqualTo(second); } private void assertKeyEquals(AnnotatedElementKey first, AnnotatedElementKey second) { diff --git a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java index e9f573c9a18d..4e18f567b6c1 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -42,17 +43,24 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.SpringProperties; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.context.expression.StandardBeanExpressionResolver.MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME; /** + * Integration tests for SpEL expression support in an {@code ApplicationContext}. + * * @author Juergen Hoeller * @author Sam Brannen * @since 3.0 @@ -196,7 +204,7 @@ public String getConversationId() { assertThat(tb6.tb).isSameAs(tb0); } finally { - System.getProperties().remove("country"); + System.clearProperty("country"); } } @@ -230,8 +238,8 @@ void prototypeCreationReevaluatesExpressions() { assertThat(tb.getCountry2()).isEqualTo("-UK2-"); } finally { - System.getProperties().remove("name"); - System.getProperties().remove("country"); + System.clearProperty("name"); + System.clearProperty("country"); } } @@ -264,7 +272,71 @@ void resourceInjection() throws IOException { assertThat(FileCopyUtils.copyToString(resourceInjectionBean.reader)).isEqualTo(FileCopyUtils.copyToString(new EncodedResource(resource).getReader())); } finally { - System.getProperties().remove("logfile"); + System.clearProperty("logfile"); + } + } + + @Test + void maxSpelExpressionLengthMustBeAnInteger() { + doWithMaxSpelExpressionLength("boom", () -> { + try (AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext()) { + assertThatIllegalArgumentException() + .isThrownBy(ac::refresh) + .withMessageStartingWith("Failed to parse value for system property [%s]", + MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME) + .withMessageContaining("boom"); + } + }); + } + + @Test + void maxSpelExpressionLengthMustBePositive() { + doWithMaxSpelExpressionLength("-99", () -> { + try (AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext()) { + assertThatIllegalArgumentException() + .isThrownBy(ac::refresh) + .withMessage("Value [%d] for system property [%s] must be positive", -99, + MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME); + } + }); + } + + @Test + void maxSpelExpressionLength() { + final String expression = "#{ 'xyz' + 'xyz' + 'xyz' }"; + + // With the default max length of 10_000, the expression should succeed. + evaluateExpressionInBean(expression); + + // With a max length of 20, the expression should fail. + doWithMaxSpelExpressionLength("20", () -> + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> evaluateExpressionInBean(expression)) + .havingRootCause() + .isInstanceOf(SpelEvaluationException.class) + .withMessageEndingWith("exceeding the threshold of '20' characters")); + } + + private static void doWithMaxSpelExpressionLength(String maxLength, Runnable action) { + try { + SpringProperties.setProperty(MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME, maxLength); + action.run(); + } + finally { + SpringProperties.setProperty(MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME, null); + } + } + + private static void evaluateExpressionInBean(String expression) { + try (AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext()) { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(String.class); + bd.getConstructorArgumentValues().addGenericArgumentValue(expression); + ac.registerBeanDefinition("str", bd); + ac.refresh(); + + String str = ac.getBean("str", String.class); + assertThat(str).isEqualTo("xyz".repeat(3)); // "#{ 'xyz' + 'xyz' + 'xyz' }" } } diff --git a/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java index ceb8da3c98a3..aa1764d5b277 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,12 +35,12 @@ /** * @author Stephane Nicoll */ -public class CachedExpressionEvaluatorTests { +class CachedExpressionEvaluatorTests { private final TestExpressionEvaluator expressionEvaluator = new TestExpressionEvaluator(); @Test - public void parseNewExpression() { + void parseNewExpression() { Method method = ReflectionUtils.findMethod(getClass(), "toString"); Expression expression = expressionEvaluator.getTestExpression("true", method, getClass()); hasParsedExpression("true"); @@ -49,7 +49,7 @@ public void parseNewExpression() { } @Test - public void cacheExpression() { + void cacheExpression() { Method method = ReflectionUtils.findMethod(getClass(), "toString"); expressionEvaluator.getTestExpression("true", method, getClass()); @@ -60,7 +60,7 @@ public void cacheExpression() { } @Test - public void cacheExpressionBasedOnConcreteType() { + void cacheExpressionBasedOnConcreteType() { Method method = ReflectionUtils.findMethod(getClass(), "toString"); expressionEvaluator.getTestExpression("true", method, getClass()); expressionEvaluator.getTestExpression("true", method, Object.class); diff --git a/spring-context/src/test/java/org/springframework/context/expression/EnvironmentAccessorIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/expression/EnvironmentAccessorIntegrationTests.java index 93bd12add15d..189e20feda5d 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/EnvironmentAccessorIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/EnvironmentAccessorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,10 +34,10 @@ * * @author Chris Beams */ -public class EnvironmentAccessorIntegrationTests { +class EnvironmentAccessorIntegrationTests { @Test - public void braceAccess() { + void braceAccess() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) diff --git a/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java b/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java index d184a07710f4..0ba7c89cbcba 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.expression.FactoryBeanAccessTests.SimpleBeanResolver.CarFactoryBean; import org.springframework.context.support.StaticApplicationContext; -import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -33,14 +32,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Unit tests for expressions accessing beans and factory beans. + * Tests for expressions accessing beans and factory beans. * * @author Andy Clement */ -public class FactoryBeanAccessTests { +class FactoryBeanAccessTests { @Test - public void factoryBeanAccess() { // SPR9511 + void factoryBeanAccess() { // SPR9511 StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new SimpleBeanResolver()); Expression expr = new SpelExpressionParser().parseRaw("@car.colour"); @@ -110,8 +109,7 @@ public SimpleBeanResolver() { } @Override - public Object resolve(EvaluationContext context, String beanName) - throws AccessException { + public Object resolve(EvaluationContext context, String beanName) { return ac.getBean(beanName); } } diff --git a/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java b/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java index 8fd4d2fb9a99..7266570b76cd 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,14 +29,14 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for compilation of {@link MapAccessor}. + * Tests for {@link MapAccessor}. * * @author Andy Clement */ -public class MapAccessorTests { +class MapAccessorTests { @Test - public void mapAccessorCompilable() { + void mapAccessorCompilable() { Map testMap = getSimpleTestMap(); StandardEvaluationContext sec = new StandardEvaluationContext(); sec.addPropertyAccessor(new MapAccessor()); diff --git a/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTests.java b/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTests.java index f2f3b151de62..fd2a8ed92159 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,19 +28,19 @@ import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN; /** - * Unit tests for {@link MethodBasedEvaluationContext}. + * Tests for {@link MethodBasedEvaluationContext}. * * @author Stephane Nicoll * @author Juergen Hoeller * @author Sergey Podgurskiy */ -public class MethodBasedEvaluationContextTests { +class MethodBasedEvaluationContextTests { private final ParameterNameDiscoverer paramDiscover = new DefaultParameterNameDiscoverer(); @Test - public void simpleArguments() { + void simpleArguments() { Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", String.class, Boolean.class); MethodBasedEvaluationContext context = createEvaluationContext(method, "test", true); @@ -57,7 +57,7 @@ public void simpleArguments() { } @Test - public void nullArgument() { + void nullArgument() { Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", String.class, Boolean.class); MethodBasedEvaluationContext context = createEvaluationContext(method, null, null); @@ -71,7 +71,7 @@ public void nullArgument() { } @Test - public void varArgEmpty() { + void varArgEmpty() { Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); MethodBasedEvaluationContext context = createEvaluationContext(method, new Object[] {null}); @@ -85,7 +85,7 @@ public void varArgEmpty() { } @Test - public void varArgNull() { + void varArgNull() { Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); MethodBasedEvaluationContext context = createEvaluationContext(method, null, null); @@ -99,7 +99,7 @@ public void varArgNull() { } @Test - public void varArgSingle() { + void varArgSingle() { Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); MethodBasedEvaluationContext context = createEvaluationContext(method, null, "hello"); @@ -113,7 +113,7 @@ public void varArgSingle() { } @Test - public void varArgMultiple() { + void varArgMultiple() { Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); MethodBasedEvaluationContext context = createEvaluationContext(method, null, "hello", "hi"); diff --git a/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java b/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java index 8034870a00e2..f15f3589ee7a 100644 --- a/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java +++ b/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,8 +59,8 @@ void generateApplicationContextWithSimpleBean() { void generateApplicationContextWithAutowiring() { GenericApplicationContext context = new AnnotationConfigApplicationContext(); context.registerBeanDefinition("autowiredComponent", new RootBeanDefinition(AutowiredComponent.class)); - context.registerBeanDefinition("number", BeanDefinitionBuilder.rootBeanDefinition(Integer.class, "valueOf") - .addConstructorArgValue("42").getBeanDefinition()); + context.registerBeanDefinition("number", BeanDefinitionBuilder.rootBeanDefinition( + Integer.class, "valueOf").addConstructorArgValue("42").getBeanDefinition()); compile(context, (hints, invocations) -> assertThat(invocations).match(hints)); } @@ -89,8 +89,10 @@ void generateApplicationContextWithInheritedDestroyMethods() { compile(context, (hints, invocations) -> assertThat(invocations).match(hints)); } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private void compile(GenericApplicationContext applicationContext, BiConsumer initializationResult) { + @SuppressWarnings({"rawtypes", "unchecked"}) + private void compile(GenericApplicationContext applicationContext, + BiConsumer initializationResult) { + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); TestGenerationContext generationContext = new TestGenerationContext(); generator.processAheadOfTime(applicationContext, generationContext); @@ -107,17 +109,15 @@ private void compile(GenericApplicationContext applicationContext, BiConsumer { CandidateComponentsTestClassLoader classLoader = new CandidateComponentsTestClassLoader(getClass().getClassLoader(), cause); diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java index ab94bfd456e5..0b0b7c55675f 100644 --- a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,12 @@ * * @author Stephane Nicoll */ +@Deprecated +@SuppressWarnings("removal") public class CandidateComponentsIndexTests { @Test - public void getCandidateTypes() { + void getCandidateTypes() { CandidateComponentsIndex index = new CandidateComponentsIndex( Collections.singletonList(createSampleProperties())); Set actual = index.getCandidateTypes("com.example.service", "service"); @@ -42,7 +44,7 @@ public void getCandidateTypes() { } @Test - public void getCandidateTypesSubPackage() { + void getCandidateTypesSubPackage() { CandidateComponentsIndex index = new CandidateComponentsIndex( Collections.singletonList(createSampleProperties())); Set actual = index.getCandidateTypes("com.example.service.sub", "service"); @@ -50,7 +52,7 @@ public void getCandidateTypesSubPackage() { } @Test - public void getCandidateTypesSubPackageNoMatch() { + void getCandidateTypesSubPackageNoMatch() { CandidateComponentsIndex index = new CandidateComponentsIndex( Collections.singletonList(createSampleProperties())); Set actual = index.getCandidateTypes("com.example.service.none", "service"); @@ -58,7 +60,7 @@ public void getCandidateTypesSubPackageNoMatch() { } @Test - public void getCandidateTypesNoMatch() { + void getCandidateTypesNoMatch() { CandidateComponentsIndex index = new CandidateComponentsIndex( Collections.singletonList(createSampleProperties())); Set actual = index.getCandidateTypes("com.example.service", "entity"); @@ -66,7 +68,7 @@ public void getCandidateTypesNoMatch() { } @Test - public void mergeCandidateStereotypes() { + void mergeCandidateStereotypes() { CandidateComponentsIndex index = new CandidateComponentsIndex(Arrays.asList( createProperties("com.example.Foo", "service"), createProperties("com.example.Foo", "entity"))); diff --git a/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java index 0bc7ca2fa4f8..7c153e3fd42c 100644 --- a/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -193,10 +193,6 @@ public int getOrder() { public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { registry.registerBeanDefinition("bfpp1", new RootBeanDefinition(TestBeanFactoryPostProcessor.class)); } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - } } @@ -224,10 +220,6 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t registry.registerBeanDefinition("anotherpp", new RootBeanDefinition(TestBeanDefinitionRegistryPostProcessor.class)); registry.registerBeanDefinition("ppp", new RootBeanDefinition(PrioritizedBeanDefinitionRegistryPostProcessor.class)); } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - } } diff --git a/spring-context/src/test/java/org/springframework/context/support/ClassPathXmlApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/ClassPathXmlApplicationContextTests.java index 44e0bcd9e346..18756397bf43 100644 --- a/spring-context/src/test/java/org/springframework/context/support/ClassPathXmlApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/ClassPathXmlApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,9 @@ * @author Juergen Hoeller * @author Chris Beams * @author Sam Brannen + * @author Yanming Zhou */ -public class ClassPathXmlApplicationContextTests { +class ClassPathXmlApplicationContextTests { private static final String PATH = "/org/springframework/context/support/"; private static final String RESOURCE_CONTEXT = PATH + "ClassPathXmlApplicationContextTests-resource.xml"; @@ -134,7 +135,7 @@ void aliasWithPlaceholder() { } @Test - void contextWithInvalidValueType() throws IOException { + void contextWithInvalidValueType() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( new String[] {INVALID_VALUE_TYPE_CONTEXT}, false); assertThatExceptionOfType(BeanCreationException.class) @@ -222,6 +223,11 @@ void resourceArrayPropertyEditor() throws IOException { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONTEXT_WILDCARD); Service service = ctx.getBean("service", Service.class); assertThat(service.getResources()).containsExactlyInAnyOrder(contextA, contextB, contextC); + assertThat(service.getResourceSet()).containsExactlyInAnyOrder(contextA, contextB, contextC); + + Service service3 = ctx.getBean("service3", Service.class); + assertThat(service3.getResources()).containsOnly(new ClassPathResource(FQ_CONTEXT_A)); + assertThat(service3.getResourceSet()).containsOnly(new ClassPathResource(FQ_CONTEXT_A)); ctx.close(); } @@ -311,17 +317,13 @@ private void assertOneMessageSourceOnly(ClassPathXmlApplicationContext ctx, Obje assertThat(beanNamesForType[0]).isEqualTo("myMessageSource"); Map beansOfType = ctx.getBeansOfType(StaticMessageSource.class); - assertThat(beansOfType).hasSize(1); - assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + assertThat(beansOfType.values()).singleElement().isSameAs(myMessageSource); beansOfType = ctx.getBeansOfType(StaticMessageSource.class, true, true); - assertThat(beansOfType).hasSize(1); - assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + assertThat(beansOfType.values()).singleElement().isSameAs(myMessageSource); beansOfType = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, StaticMessageSource.class); - assertThat(beansOfType).hasSize(1); - assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + assertThat(beansOfType.values()).singleElement().isSameAs(myMessageSource); beansOfType = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, StaticMessageSource.class, true, true); - assertThat(beansOfType).hasSize(1); - assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + assertThat(beansOfType.values()).singleElement().isSameAs(myMessageSource); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java index 697e4fc834a3..90e3ccba6040 100644 --- a/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,7 @@ void createDefaultConversionService() { } @Test + @SuppressWarnings("Convert2Lambda") void createDefaultConversionServiceWithSupplements() { ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean(); Set converters = new HashSet<>(); @@ -117,14 +118,14 @@ void conversionServiceInApplicationContextWithResourceOverriding() { private void doTestConversionServiceInApplicationContext(String fileName, Class resourceClass) { ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(fileName, getClass()); ResourceTestBean tb = ctx.getBean("resourceTestBean", ResourceTestBean.class); - assertThat(resourceClass.isInstance(tb.getResource())).isTrue(); + assertThat(tb.getResource()).isInstanceOf(resourceClass); assertThat(tb.getResourceArray()).hasSize(1); - assertThat(resourceClass.isInstance(tb.getResourceArray()[0])).isTrue(); + assertThat(tb.getResourceArray()[0]).isInstanceOf(resourceClass); assertThat(tb.getResourceMap()).hasSize(1); - assertThat(resourceClass.isInstance(tb.getResourceMap().get("key1"))).isTrue(); + assertThat(tb.getResourceMap().get("key1")).isInstanceOf(resourceClass); assertThat(tb.getResourceArrayMap()).hasSize(1); assertThat(tb.getResourceArrayMap().get("key1")).isNotEmpty(); - assertThat(resourceClass.isInstance(tb.getResourceArrayMap().get("key1")[0])).isTrue(); + assertThat(tb.getResourceArrayMap().get("key1")[0]).isInstanceOf(resourceClass); ctx.close(); } @@ -141,7 +142,7 @@ static class Baz { static class ComplexConstructorArgument { ComplexConstructorArgument(Map> map) { - assertThat(map.isEmpty()).isFalse(); + assertThat(map).isNotEmpty(); assertThat(map.keySet().iterator().next()).isInstanceOf(String.class); assertThat(map.values().iterator().next()).isInstanceOf(Class.class); } diff --git a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java index 9bcfa4e7a692..da666fea6ec4 100644 --- a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.context.support; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -24,16 +25,19 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContextException; import org.springframework.context.Lifecycle; import org.springframework.context.LifecycleProcessor; import org.springframework.context.SmartLifecycle; import org.springframework.core.testfixture.EnabledForTestGroups; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; /** * @author Mark Fisher + * @author Juergen Hoeller * @since 3.0 */ class DefaultLifecycleProcessorTests { @@ -45,6 +49,7 @@ void defaultLifecycleProcessorInstance() { Object lifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); assertThat(lifecycleProcessor).isNotNull(); assertThat(lifecycleProcessor.getClass()).isEqualTo(DefaultLifecycleProcessor.class); + context.close(); } @Test @@ -58,12 +63,13 @@ void customLifecycleProcessorInstance() { Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); assertThat(contextLifecycleProcessor).isNotNull(); assertThat(contextLifecycleProcessor).isSameAs(bean); - assertThat(new DirectFieldAccessor(contextLifecycleProcessor).getPropertyValue( - "timeoutPerShutdownPhase")).isEqualTo(1000L); + assertThat(new DirectFieldAccessor(contextLifecycleProcessor).getPropertyValue("timeoutPerShutdownPhase")) + .isEqualTo(1000L); + context.close(); } @Test - void singleSmartLifecycleAutoStartup() throws Exception { + void singleSmartLifecycleAutoStartup() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); @@ -79,7 +85,7 @@ void singleSmartLifecycleAutoStartup() throws Exception { } @Test - void singleSmartLifecycleAutoStartupWithLazyInit() throws Exception { + void singleSmartLifecycleAutoStartupWithLazyInit() { StaticApplicationContext context = new StaticApplicationContext(); RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleBean.class); bd.setLazyInit(true); @@ -93,7 +99,7 @@ void singleSmartLifecycleAutoStartupWithLazyInit() throws Exception { } @Test - void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() throws Exception { + void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() { StaticApplicationContext context = new StaticApplicationContext(); RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleFactoryBean.class); bd.setLazyInit(true); @@ -107,7 +113,23 @@ void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() throws Exception { } @Test - void singleSmartLifecycleWithoutAutoStartup() throws Exception { + void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + bean.setAutoStartup(true); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean", bean); + context.registerSingleton("failingBean", FailingLifecycleBean.class); + assertThat(bean.isRunning()).isFalse(); + assertThatExceptionOfType(ApplicationContextException.class) + .isThrownBy(context::refresh).withCauseInstanceOf(IllegalStateException.class); + assertThat(bean.isRunning()).isFalse(); + assertThat(startedBeans).hasSize(1); + context.close(); + } + + @Test + void singleSmartLifecycleWithoutAutoStartup() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(false); @@ -125,7 +147,7 @@ void singleSmartLifecycleWithoutAutoStartup() throws Exception { } @Test - void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() throws Exception { + void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); @@ -148,7 +170,7 @@ void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() throws Except } @Test - void smartLifecycleGroupStartup() throws Exception { + void smartLifecycleGroupStartup() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forStartupTests(1, startedBeans); @@ -173,17 +195,13 @@ void smartLifecycleGroupStartup() throws Exception { assertThat(bean3.isRunning()).isTrue(); assertThat(beanMax.isRunning()).isTrue(); context.stop(); - assertThat(startedBeans).hasSize(5); - assertThat(getPhase(startedBeans.get(0))).isEqualTo(Integer.MIN_VALUE); - assertThat(getPhase(startedBeans.get(1))).isEqualTo(1); - assertThat(getPhase(startedBeans.get(2))).isEqualTo(2); - assertThat(getPhase(startedBeans.get(3))).isEqualTo(3); - assertThat(getPhase(startedBeans.get(4))).isEqualTo(Integer.MAX_VALUE); + assertThat(startedBeans).satisfiesExactly(hasPhase(Integer.MIN_VALUE),hasPhase(1), + hasPhase(2), hasPhase(3), hasPhase(Integer.MAX_VALUE)); context.close(); } @Test - void contextRefreshThenStartWithMixedBeans() throws Exception { + void contextRefreshThenStartWithMixedBeans() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); @@ -203,22 +221,18 @@ void contextRefreshThenStartWithMixedBeans() throws Exception { assertThat(smartBean2.isRunning()).isTrue(); assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); - assertThat(startedBeans).hasSize(2); - assertThat(getPhase(startedBeans.get(0))).isEqualTo(-3); - assertThat(getPhase(startedBeans.get(1))).isEqualTo(5); + assertThat(startedBeans).satisfiesExactly(hasPhase(-3), hasPhase(5)); context.start(); assertThat(smartBean1.isRunning()).isTrue(); assertThat(smartBean2.isRunning()).isTrue(); assertThat(simpleBean1.isRunning()).isTrue(); assertThat(simpleBean2.isRunning()).isTrue(); - assertThat(startedBeans).hasSize(4); - assertThat(getPhase(startedBeans.get(2))).isEqualTo(0); - assertThat(getPhase(startedBeans.get(3))).isEqualTo(0); + assertThat(startedBeans).satisfiesExactly(hasPhase(-3), hasPhase(5), hasPhase(0), hasPhase(0)); context.close(); } @Test - void contextRefreshThenStopAndRestartWithMixedBeans() throws Exception { + void contextRefreshThenStopAndRestartWithMixedBeans() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); @@ -238,9 +252,7 @@ void contextRefreshThenStopAndRestartWithMixedBeans() throws Exception { assertThat(smartBean2.isRunning()).isTrue(); assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); - assertThat(startedBeans).hasSize(2); - assertThat(getPhase(startedBeans.get(0))).isEqualTo(-3); - assertThat(getPhase(startedBeans.get(1))).isEqualTo(5); + assertThat(startedBeans).satisfiesExactly(hasPhase(-3), hasPhase(5)); context.stop(); assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); @@ -251,17 +263,62 @@ void contextRefreshThenStopAndRestartWithMixedBeans() throws Exception { assertThat(smartBean2.isRunning()).isTrue(); assertThat(simpleBean1.isRunning()).isTrue(); assertThat(simpleBean2.isRunning()).isTrue(); - assertThat(startedBeans).hasSize(6); - assertThat(getPhase(startedBeans.get(2))).isEqualTo(-3); - assertThat(getPhase(startedBeans.get(3))).isEqualTo(0); - assertThat(getPhase(startedBeans.get(4))).isEqualTo(0); - assertThat(getPhase(startedBeans.get(5))).isEqualTo(5); + assertThat(startedBeans).satisfiesExactly(hasPhase(-3), hasPhase(5), + hasPhase(-3), hasPhase(0), hasPhase(0), hasPhase(5)); + context.close(); + } + + @Test + void contextRefreshThenStopForRestartWithMixedBeans() { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); + TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); + TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); + TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); + context.getBeanFactory().registerSingleton("smartBean1", smartBean1); + context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); + context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(smartBean1.isRunning()).isFalse(); + assertThat(smartBean2.isRunning()).isFalse(); + context.refresh(); + DefaultLifecycleProcessor lifecycleProcessor = (DefaultLifecycleProcessor) + new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + smartBean2.stop(); + simpleBean1.start(); + assertThat(startedBeans).satisfiesExactly(hasPhase(-3), hasPhase(5), hasPhase(0)); + lifecycleProcessor.stopForRestart(); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(smartBean1.isRunning()).isFalse(); + assertThat(smartBean2.isRunning()).isFalse(); + lifecycleProcessor.restartAfterStop(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isFalse(); + assertThat(simpleBean1.isRunning()).isTrue(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(startedBeans).satisfiesExactly(hasPhase(-3), hasPhase(5), + hasPhase(0), hasPhase(0), hasPhase(5)); + context.start(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isTrue(); + assertThat(simpleBean2.isRunning()).isTrue(); + assertThat(startedBeans).satisfiesExactly(hasPhase(-3), hasPhase(5), + hasPhase(0), hasPhase(0), hasPhase(5), hasPhase(-3), hasPhase(0)); context.close(); } @Test @EnabledForTestGroups(LONG_RUNNING) - void smartLifecycleGroupShutdown() throws Exception { + void smartLifecycleGroupShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans); @@ -280,19 +337,14 @@ void smartLifecycleGroupShutdown() throws Exception { context.getBeanFactory().registerSingleton("bean7", bean7); context.refresh(); context.stop(); - assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(Integer.MAX_VALUE); - assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(3); - assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(3); - assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(2); - assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(2); - assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(1); - assertThat(getPhase(stoppedBeans.get(6))).isEqualTo(1); + assertThat(stoppedBeans).satisfiesExactly(hasPhase(Integer.MAX_VALUE), hasPhase(3), + hasPhase(3), hasPhase(2), hasPhase(2), hasPhase(1), hasPhase(1)); context.close(); } @Test @EnabledForTestGroups(LONG_RUNNING) - void singleSmartLifecycleShutdown() throws Exception { + void singleSmartLifecycleShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans); StaticApplicationContext context = new StaticApplicationContext(); @@ -300,14 +352,13 @@ void singleSmartLifecycleShutdown() throws Exception { context.refresh(); assertThat(bean.isRunning()).isTrue(); context.stop(); - assertThat(stoppedBeans).hasSize(1); assertThat(bean.isRunning()).isFalse(); - assertThat(stoppedBeans.get(0)).isEqualTo(bean); + assertThat(stoppedBeans).containsExactly(bean); context.close(); } @Test - void singleLifecycleShutdown() throws Exception { + void singleLifecycleShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean = new TestLifecycleBean(null, stoppedBeans); StaticApplicationContext context = new StaticApplicationContext(); @@ -317,14 +368,13 @@ void singleLifecycleShutdown() throws Exception { bean.start(); assertThat(bean.isRunning()).isTrue(); context.stop(); - assertThat(stoppedBeans).hasSize(1); assertThat(bean.isRunning()).isFalse(); - assertThat(stoppedBeans.get(0)).isEqualTo(bean); + assertThat(stoppedBeans).singleElement().isEqualTo(bean); context.close(); } @Test - void mixedShutdown() throws Exception { + void mixedShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans); Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans); @@ -361,19 +411,13 @@ void mixedShutdown() throws Exception { assertThat(bean5.isRunning()).isFalse(); assertThat(bean6.isRunning()).isFalse(); assertThat(bean7.isRunning()).isFalse(); - assertThat(stoppedBeans).hasSize(7); - assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(Integer.MAX_VALUE); - assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(500); - assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(1); - assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(0); - assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(0); - assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(-1); - assertThat(getPhase(stoppedBeans.get(6))).isEqualTo(Integer.MIN_VALUE); + assertThat(stoppedBeans).satisfiesExactly(hasPhase(Integer.MAX_VALUE), hasPhase(500), + hasPhase(1), hasPhase(0), hasPhase(0), hasPhase(-1), hasPhase(Integer.MIN_VALUE)); context.close(); } @Test - void dependencyStartedFirstEvenIfItsPhaseIsHigher() throws Exception { + void dependencyStartedFirstEvenIfItsPhaseIsHigher() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); @@ -390,20 +434,18 @@ void dependencyStartedFirstEvenIfItsPhaseIsHigher() throws Exception { assertThat(bean2.isRunning()).isTrue(); assertThat(bean99.isRunning()).isTrue(); assertThat(beanMax.isRunning()).isTrue(); - assertThat(startedBeans).hasSize(4); - assertThat(getPhase(startedBeans.get(0))).isEqualTo(Integer.MIN_VALUE); - assertThat(getPhase(startedBeans.get(1))).isEqualTo(99); - assertThat(startedBeans.get(1)).isEqualTo(bean99); - assertThat(getPhase(startedBeans.get(2))).isEqualTo(2); - assertThat(startedBeans.get(2)).isEqualTo(bean2); - assertThat(getPhase(startedBeans.get(3))).isEqualTo(Integer.MAX_VALUE); + assertThat(startedBeans).satisfiesExactly( + hasPhase(Integer.MIN_VALUE), + one -> assertThat(one).isEqualTo(bean99).satisfies(hasPhase(99)), + two -> assertThat(two).isEqualTo(bean2).satisfies(hasPhase(2)), + hasPhase(Integer.MAX_VALUE)); context.stop(); context.close(); } @Test @EnabledForTestGroups(LONG_RUNNING) - void dependentShutdownFirstEvenIfItsPhaseIsLower() throws Exception { + void dependentShutdownFirstEvenIfItsPhaseIsLower() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); @@ -434,19 +476,16 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() throws Exception { assertThat(bean99.isRunning()).isFalse(); assertThat(beanMax.isRunning()).isFalse(); assertThat(stoppedBeans).hasSize(6); - assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(Integer.MAX_VALUE); - assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(2); - assertThat(stoppedBeans.get(1)).isEqualTo(bean2); - assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(99); - assertThat(stoppedBeans.get(2)).isEqualTo(bean99); - assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(7); - assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(1); - assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(Integer.MIN_VALUE); + assertThat(stoppedBeans).satisfiesExactly( + hasPhase(Integer.MAX_VALUE), + one -> assertThat(one).isEqualTo(bean2).satisfies(hasPhase(2)), + two -> assertThat(two).isEqualTo(bean99).satisfies(hasPhase(99)), + hasPhase(7), hasPhase(1), hasPhase(Integer.MIN_VALUE)); context.close(); } @Test - void dependencyStartedFirstAndIsSmartLifecycle() throws Exception { + void dependencyStartedFirstAndIsSmartLifecycle() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); @@ -467,18 +506,14 @@ void dependencyStartedFirstAndIsSmartLifecycle() throws Exception { assertThat(bean99.isRunning()).isTrue(); assertThat(bean7.isRunning()).isTrue(); assertThat(simpleBean.isRunning()).isTrue(); - assertThat(startedBeans).hasSize(4); - assertThat(getPhase(startedBeans.get(0))).isEqualTo(-99); - assertThat(getPhase(startedBeans.get(1))).isEqualTo(7); - assertThat(getPhase(startedBeans.get(2))).isEqualTo(0); - assertThat(getPhase(startedBeans.get(3))).isEqualTo(99); + assertThat(startedBeans).satisfiesExactly(hasPhase(-99), hasPhase(7), hasPhase(0), hasPhase(99)); context.stop(); context.close(); } @Test @EnabledForTestGroups(LONG_RUNNING) - void dependentShutdownFirstAndIsSmartLifecycle() throws Exception { + void dependentShutdownFirstAndIsSmartLifecycle() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans); @@ -509,18 +544,13 @@ void dependentShutdownFirstAndIsSmartLifecycle() throws Exception { assertThat(bean2.isRunning()).isFalse(); assertThat(bean7.isRunning()).isFalse(); assertThat(simpleBean.isRunning()).isFalse(); - assertThat(stoppedBeans).hasSize(6); - assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(7); - assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(2); - assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(1); - assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(-99); - assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(0); - assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(Integer.MIN_VALUE); + assertThat(stoppedBeans).satisfiesExactly(hasPhase(7), hasPhase(2), + hasPhase(1), hasPhase(-99), hasPhase(0), hasPhase(Integer.MIN_VALUE)); context.close(); } @Test - void dependencyStartedFirstButNotSmartLifecycle() throws Exception { + void dependencyStartedFirstButNotSmartLifecycle() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); @@ -534,17 +564,14 @@ void dependencyStartedFirstButNotSmartLifecycle() throws Exception { assertThat(beanMin.isRunning()).isTrue(); assertThat(bean7.isRunning()).isTrue(); assertThat(simpleBean.isRunning()).isTrue(); - assertThat(startedBeans).hasSize(3); - assertThat(getPhase(startedBeans.get(0))).isEqualTo(0); - assertThat(getPhase(startedBeans.get(1))).isEqualTo(Integer.MIN_VALUE); - assertThat(getPhase(startedBeans.get(2))).isEqualTo(7); + assertThat(startedBeans).satisfiesExactly(hasPhase(0), hasPhase(Integer.MIN_VALUE), hasPhase(7)); context.stop(); context.close(); } @Test @EnabledForTestGroups(LONG_RUNNING) - void dependentShutdownFirstButNotSmartLifecycle() throws Exception { + void dependentShutdownFirstButNotSmartLifecycle() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); @@ -572,22 +599,18 @@ void dependentShutdownFirstButNotSmartLifecycle() throws Exception { assertThat(bean2.isRunning()).isFalse(); assertThat(bean7.isRunning()).isFalse(); assertThat(simpleBean.isRunning()).isFalse(); - assertThat(stoppedBeans).hasSize(5); - assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(7); - assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(0); - assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(2); - assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(1); - assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(Integer.MIN_VALUE); + assertThat(stoppedBeans).satisfiesExactly(hasPhase(7), hasPhase(0), + hasPhase(2), hasPhase(1), hasPhase(Integer.MIN_VALUE)); context.close(); } - - private static int getPhase(Lifecycle lifecycle) { - return (lifecycle instanceof SmartLifecycle) ? - ((SmartLifecycle) lifecycle).getPhase() : 0; + private Consumer hasPhase(int phase) { + return lifecycle -> { + int actual = lifecycle instanceof SmartLifecycle smartLifecycle ? smartLifecycle.getPhase() : 0; + assertThat(actual).isEqualTo(phase); + }; } - private static class TestLifecycleBean implements Lifecycle { private final CopyOnWriteArrayList startedBeans; @@ -596,7 +619,6 @@ private static class TestLifecycleBean implements Lifecycle { private volatile boolean running; - static TestLifecycleBean forStartupTests(CopyOnWriteArrayList startedBeans) { return new TestLifecycleBean(startedBeans, null); } @@ -605,7 +627,7 @@ static TestLifecycleBean forShutdownTests(CopyOnWriteArrayList stoppe return new TestLifecycleBean(null, stoppedBeans); } - private TestLifecycleBean(CopyOnWriteArrayList startedBeans, CopyOnWriteArrayList stoppedBeans) { + private TestLifecycleBean(CopyOnWriteArrayList startedBeans, CopyOnWriteArrayList stoppedBeans) { this.startedBeans = startedBeans; this.stoppedBeans = stoppedBeans; } @@ -649,7 +671,8 @@ static TestSmartLifecycleBean forShutdownTests(int phase, int shutdownDelay, Cop return new TestSmartLifecycleBean(phase, shutdownDelay, null, stoppedBeans); } - private TestSmartLifecycleBean(int phase, int shutdownDelay, CopyOnWriteArrayList startedBeans, CopyOnWriteArrayList stoppedBeans) { + private TestSmartLifecycleBean(int phase, int shutdownDelay, CopyOnWriteArrayList startedBeans, + CopyOnWriteArrayList stoppedBeans) { super(startedBeans, stoppedBeans); this.phase = phase; this.shutdownDelay = shutdownDelay; @@ -734,7 +757,7 @@ public static class DummySmartLifecycleFactoryBean implements FactoryBean, ApplicationListener { @Override - public String getObject() throws Exception { + public String getObject() { return ""; } diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java index dbcbb90cffc7..c96b05b3b1a1 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,11 +29,16 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.GenericBeanDefinition; @@ -58,6 +63,7 @@ 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.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -126,16 +132,16 @@ void accessAfterClosing() { assertThat(context.getBean(String.class)).isSameAs(context.getBean("testBean")); assertThat(context.getAutowireCapableBeanFactory().getBean(String.class)) - .isSameAs(context.getAutowireCapableBeanFactory().getBean("testBean")); + .isSameAs(context.getAutowireCapableBeanFactory().getBean("testBean")); context.close(); assertThatIllegalStateException() - .isThrownBy(() -> context.getBean(String.class)); + .isThrownBy(() -> context.getBean(String.class)); assertThatIllegalStateException() - .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean(String.class)); + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean(String.class)); assertThatIllegalStateException() - .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean("testBean")); + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean("testBean")); } @Test @@ -236,9 +242,9 @@ void individualBeanWithNullReturningSupplier() { assertThat(context.getBeanNamesForType(BeanB.class)).containsExactly("b"); assertThat(context.getBeanNamesForType(BeanC.class)).containsExactly("c"); assertThat(context.getBeansOfType(BeanA.class)).isEmpty(); - assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()) + assertThat(context.getBeansOfType(BeanB.class).values()).singleElement() .isSameAs(context.getBean(BeanB.class)); - assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()) + assertThat(context.getBeansOfType(BeanC.class).values()).singleElement() .isSameAs(context.getBean(BeanC.class)); } @@ -281,8 +287,8 @@ private void assertGetResourceSemantics(ResourceLoader resourceLoader, Class at index 4: ping:foo if (resourceLoader instanceof FileSystemResourceLoader && OS.WINDOWS.isCurrentOs()) { assertThatExceptionOfType(InvalidPathException.class) - .isThrownBy(() -> context.getResource(pingLocation)) - .withMessageContaining(pingLocation); + .isThrownBy(() -> context.getResource(pingLocation)) + .withMessageContaining(pingLocation); } else { resource = context.getResource(pingLocation); @@ -297,8 +303,41 @@ private void assertGetResourceSemantics(ResourceLoader resourceLoader, Class new String(bar.getByteArray(), UTF_8)) - .isEqualTo("pong:foo"); + .extracting(bar -> new String(bar.getByteArray(), UTF_8)) + .isEqualTo("pong:foo"); + } + + @Test + void refreshWithRuntimeFailureOnBeanCreationDisposeExistingBeans() { + BeanE one = new BeanE(); + context.registerBean("one", BeanE.class, () -> one); + context.registerBean("two", BeanE.class, () -> new BeanE() { + @Override + public void afterPropertiesSet() { + throw new IllegalStateException("Expected"); + } + }); + assertThatThrownBy(context::refresh).isInstanceOf(BeanCreationException.class) + .hasMessageContaining("two"); + assertThat(one.initialized).isTrue(); + assertThat(one.destroyed).isTrue(); + } + + @Test + void refreshWithRuntimeFailureOnAfterSingletonInstantiatedDisposeExistingBeans() { + BeanE one = new BeanE(); + BeanE two = new BeanE(); + context.registerBean("one", BeanE.class, () -> one); + context.registerBean("two", BeanE.class, () -> two); + context.registerBean("int", SmartInitializingSingleton.class, () -> () -> { + throw new IllegalStateException("expected"); + }); + assertThatThrownBy(context::refresh).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("expected"); + assertThat(one.initialized).isTrue(); + assertThat(two.initialized).isTrue(); + assertThat(one.destroyed).isTrue(); + assertThat(two.destroyed).isTrue(); } @Test @@ -330,7 +369,7 @@ void refreshForAotLoadsBeanClassName() { } @Test - void refreshForAotLoadsBeanClassNameOfConstructorArgumentInnerBeanDefinition() { + void refreshForAotLoadsBeanClassNameOfIndexedConstructorArgumentInnerBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); GenericBeanDefinition innerBeanDefinition = new GenericBeanDefinition(); @@ -346,6 +385,23 @@ void refreshForAotLoadsBeanClassNameOfConstructorArgumentInnerBeanDefinition() { context.close(); } + @Test + void refreshForAotLoadsBeanClassNameOfGenericConstructorArgumentInnerBeanDefinition() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + GenericBeanDefinition innerBeanDefinition = new GenericBeanDefinition(); + innerBeanDefinition.setBeanClassName("java.lang.Integer"); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(innerBeanDefinition); + context.registerBeanDefinition("test",beanDefinition); + context.refreshForAotProcessing(new RuntimeHints()); + RootBeanDefinition bd = getBeanDefinition(context, "test"); + GenericBeanDefinition value = (GenericBeanDefinition) bd.getConstructorArgumentValues() + .getGenericArgumentValues().get(0).getValue(); + assertThat(value.hasBeanClass()).isTrue(); + assertThat(value.getBeanClass()).isEqualTo(Integer.class); + context.close(); + } + @Test void refreshForAotLoadsBeanClassNameOfPropertyValueInnerBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); @@ -362,6 +418,49 @@ void refreshForAotLoadsBeanClassNameOfPropertyValueInnerBeanDefinition() { context.close(); } + @Test + void refreshForAotLoadsTypedStringValueClassNameInProperty() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition beanDefinition = new RootBeanDefinition("java.lang.Integer"); + beanDefinition.getPropertyValues().add("value", new TypedStringValue("42", "java.lang.Integer")); + context.registerBeanDefinition("number", beanDefinition); + context.refreshForAotProcessing(new RuntimeHints()); + assertThat(getBeanDefinition(context, "number").getPropertyValues().get("value")) + .isInstanceOfSatisfying(TypedStringValue.class, typeStringValue -> + assertThat(typeStringValue.getTargetType()).isEqualTo(Integer.class)); + context.close(); + } + + @Test + void refreshForAotLoadsTypedStringValueClassNameInIndexedConstructorArgument() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition beanDefinition = new RootBeanDefinition("java.lang.Integer"); + beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, + new TypedStringValue("42", "java.lang.Integer")); + context.registerBeanDefinition("number", beanDefinition); + context.refreshForAotProcessing(new RuntimeHints()); + assertThat(getBeanDefinition(context, "number").getConstructorArgumentValues() + .getIndexedArgumentValue(0, TypedStringValue.class).getValue()) + .isInstanceOfSatisfying(TypedStringValue.class, typeStringValue -> + assertThat(typeStringValue.getTargetType()).isEqualTo(Integer.class)); + context.close(); + } + + @Test + void refreshForAotLoadsTypedStringValueClassNameInGenericConstructorArgument() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition beanDefinition = new RootBeanDefinition("java.lang.Integer"); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue( + new TypedStringValue("42", "java.lang.Integer")); + context.registerBeanDefinition("number", beanDefinition); + context.refreshForAotProcessing(new RuntimeHints()); + assertThat(getBeanDefinition(context, "number").getConstructorArgumentValues() + .getGenericArgumentValue(TypedStringValue.class).getValue()) + .isInstanceOfSatisfying(TypedStringValue.class, typeStringValue -> + assertThat(typeStringValue.getTargetType()).isEqualTo(Integer.class)); + context.close(); + } + @Test void refreshForAotInvokesBeanFactoryPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); @@ -385,7 +484,7 @@ void refreshForAotInvokesMergedBeanDefinitionPostProcessors() { } @Test - void refreshForAotInvokesMergedBeanDefinitionPostProcessorsOnConstructorArgument() { + void refreshForAotInvokesMergedBeanDefinitionPostProcessorsOnIndexedConstructorArgument() { GenericApplicationContext context = new GenericApplicationContext(); RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanD.class); GenericBeanDefinition innerBeanDefinition = new GenericBeanDefinition(); @@ -401,6 +500,23 @@ void refreshForAotInvokesMergedBeanDefinitionPostProcessorsOnConstructorArgument context.close(); } + @Test + void refreshForAotInvokesMergedBeanDefinitionPostProcessorsOnGenericConstructorArgument() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanD.class); + GenericBeanDefinition innerBeanDefinition = new GenericBeanDefinition(); + innerBeanDefinition.setBeanClassName("java.lang.Integer"); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(innerBeanDefinition); + context.registerBeanDefinition("test", beanDefinition); + MergedBeanDefinitionPostProcessor bpp = registerMockMergedBeanDefinitionPostProcessor(context); + context.refreshForAotProcessing(new RuntimeHints()); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(bpp).postProcessMergedBeanDefinition(getBeanDefinition(context, "test"), BeanD.class, "test"); + verify(bpp).postProcessMergedBeanDefinition(any(RootBeanDefinition.class), eq(Integer.class), captor.capture()); + assertThat(captor.getValue()).startsWith("(inner bean)"); + context.close(); + } + @Test void refreshForAotInvokesMergedBeanDefinitionPostProcessorsOnPropertyValue() { GenericApplicationContext context = new GenericApplicationContext(); @@ -536,7 +652,7 @@ public BeanA(BeanB b, BeanC c) { } } - static class BeanB implements ApplicationContextAware { + static class BeanB implements ApplicationContextAware { ApplicationContext applicationContext; @@ -568,6 +684,29 @@ public void setCounter(Integer counter) { } } + static class BeanE implements InitializingBean, DisposableBean { + + private boolean initialized; + + private boolean destroyed; + + @Override + public void afterPropertiesSet() { + if (initialized) { + throw new IllegalStateException("AfterPropertiesSet called twice"); + } + this.initialized = true; + } + + @Override + public void destroy() { + if (destroyed) { + throw new IllegalStateException("Destroy called twice"); + } + this.destroyed = true; + } + } + static class TestAotFactoryBean extends AbstractFactoryBean { diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericXmlApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericXmlApplicationContextTests.java index a14ea0f1e185..454c50b69814 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericXmlApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericXmlApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link GenericXmlApplicationContext}. + * Tests for {@link GenericXmlApplicationContext}. * * See SPR-7530. * diff --git a/spring-context/src/test/java/org/springframework/context/support/NoOpAdvice.java b/spring-context/src/test/java/org/springframework/context/support/NoOpAdvice.java index 0ade47c074d3..a8822483170d 100644 --- a/spring-context/src/test/java/org/springframework/context/support/NoOpAdvice.java +++ b/spring-context/src/test/java/org/springframework/context/support/NoOpAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ */ public class NoOpAdvice implements ThrowsAdvice { - public void afterThrowing(Exception ex) throws Throwable { + public void afterThrowing(Exception ex) { // no-op } diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java index 05e000925585..33e27414c46f 100644 --- a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,10 +52,10 @@ * @author Sam Brannen * @since 3.1 */ -public class PropertySourcesPlaceholderConfigurerTests { +class PropertySourcesPlaceholderConfigurerTests { @Test - public void replacementFromEnvironmentProperties() { + void replacementFromEnvironmentProperties() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) @@ -73,7 +73,7 @@ public void replacementFromEnvironmentProperties() { } @Test - public void localPropertiesViaResource() { + void localPropertiesViaResource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) @@ -88,17 +88,17 @@ public void localPropertiesViaResource() { } @Test - public void localPropertiesOverrideFalse() { + void localPropertiesOverrideFalse() { localPropertiesOverride(false); } @Test - public void localPropertiesOverrideTrue() { + void localPropertiesOverrideTrue() { localPropertiesOverride(true); } @Test - public void explicitPropertySources() { + void explicitPropertySources() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) @@ -112,11 +112,11 @@ public void explicitPropertySources() { ppc.setPropertySources(propertySources); ppc.postProcessBeanFactory(bf); assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo"); - assertThat(propertySources.iterator().next()).isEqualTo(ppc.getAppliedPropertySources().iterator().next()); + assertThat(propertySources).containsExactlyElementsOf(ppc.getAppliedPropertySources()); } @Test - public void explicitPropertySourcesExcludesEnvironment() { + void explicitPropertySourcesExcludesEnvironment() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) @@ -132,7 +132,7 @@ public void explicitPropertySourcesExcludesEnvironment() { ppc.setIgnoreUnresolvablePlaceholders(true); ppc.postProcessBeanFactory(bf); assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${my.name}"); - assertThat(propertySources.iterator().next()).isEqualTo(ppc.getAppliedPropertySources().iterator().next()); + assertThat(propertySources).containsExactlyElementsOf(ppc.getAppliedPropertySources()); } @Test @@ -158,7 +158,7 @@ public void explicitPropertySourcesExcludesLocalProperties() { } @Test - public void ignoreUnresolvablePlaceholders_falseIsDefault() { + void ignoreUnresolvablePlaceholders_falseIsDefault() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) @@ -175,7 +175,7 @@ public void ignoreUnresolvablePlaceholders_falseIsDefault() { } @Test - public void ignoreUnresolvablePlaceholders_true() { + void ignoreUnresolvablePlaceholders_true() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) @@ -256,7 +256,7 @@ public void ignoredNestedUnresolvablePlaceholder() { } @Test - public void withNonEnumerablePropertySource() { + void withNonEnumerablePropertySource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) @@ -305,7 +305,7 @@ private void localPropertiesOverride(boolean override) { } @Test - public void customPlaceholderPrefixAndSuffix() { + void customPlaceholderPrefixAndSuffix() { PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); ppc.setPlaceholderPrefix("@<"); ppc.setPlaceholderSuffix(">"); @@ -329,7 +329,7 @@ public void customPlaceholderPrefixAndSuffix() { } @Test - public void nullValueIsPreserved() { + void nullValueIsPreserved() { PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); ppc.setNullValue("customNull"); DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -342,7 +342,7 @@ public void nullValueIsPreserved() { } @Test - public void trimValuesIsOffByDefault() { + void trimValuesIsOffByDefault() { PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) @@ -354,7 +354,7 @@ public void trimValuesIsOffByDefault() { } @Test - public void trimValuesIsApplied() { + void trimValuesIsApplied() { PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); ppc.setTrimValues(true); DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -367,14 +367,14 @@ public void trimValuesIsApplied() { } @Test - public void getAppliedPropertySourcesTooEarly() throws Exception { + void getAppliedPropertySourcesTooEarly() { PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); assertThatIllegalStateException().isThrownBy( ppc::getAppliedPropertySources); } @Test - public void multipleLocationsWithDefaultResolvedValue() throws Exception { + void multipleLocationsWithDefaultResolvedValue() { // SPR-10619 PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); ClassPathResource doesNotHave = new ClassPathResource("test.properties", getClass()); @@ -392,7 +392,7 @@ public void multipleLocationsWithDefaultResolvedValue() throws Exception { } @Test - public void optionalPropertyWithValue() { + void optionalPropertyWithValue() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setConversionService(new DefaultConversionService()); bf.registerBeanDefinition("testBean", @@ -411,7 +411,7 @@ public void optionalPropertyWithValue() { } @Test - public void optionalPropertyWithoutValue() { + void optionalPropertyWithoutValue() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setConversionService(new DefaultConversionService()); bf.registerBeanDefinition("testBean", diff --git a/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java index d799515796d1..1ade17189d9e 100644 --- a/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.support; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Properties; @@ -31,9 +32,12 @@ 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.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 03.02.2004 */ class ResourceBundleMessageSourceTests { @@ -122,14 +126,13 @@ protected void doTestMessageAccess( if (alwaysUseMessageFormat) { pvs.add("alwaysUseMessageFormat", Boolean.TRUE); } - Class clazz = reloadable ? - (Class) ReloadableResourceBundleMessageSource.class : ResourceBundleMessageSource.class; + Class clazz = reloadable ? ReloadableResourceBundleMessageSource.class : ResourceBundleMessageSource.class; ac.registerSingleton("messageSource", clazz, pvs); ac.refresh(); Locale.setDefault(expectGermanFallback ? Locale.GERMAN : Locale.CANADA); assertThat(ac.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); - Object expected = fallbackToSystemLocale && expectGermanFallback ? "nachricht2" : "message2"; + Object expected = (fallbackToSystemLocale && expectGermanFallback ? "nachricht2" : "message2"); assertThat(ac.getMessage("code2", null, Locale.ENGLISH)).isEqualTo(expected); assertThat(ac.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); @@ -207,6 +210,8 @@ void defaultApplicationContextMessageSource() { ac.refresh(); assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + ac.close(); + assertThatIllegalStateException().isThrownBy(() -> ac.getMessage("code1", null, "default", Locale.ENGLISH)); } @Test @@ -219,6 +224,8 @@ void defaultApplicationContextMessageSourceWithParent() { ac.refresh(); assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + ac.close(); + assertThatIllegalStateException().isThrownBy(() -> ac.getMessage("code1", null, "default", Locale.ENGLISH)); } @Test @@ -231,6 +238,8 @@ void staticApplicationContextMessageSourceWithStaticParent() { ac.refresh(); assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + ac.close(); + assertThatIllegalStateException().isThrownBy(() -> ac.getMessage("code1", null, "default", Locale.ENGLISH)); } @Test @@ -243,6 +252,8 @@ void staticApplicationContextMessageSourceWithDefaultParent() { ac.refresh(); assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + ac.close(); + assertThatIllegalStateException().isThrownBy(() -> ac.getMessage("code1", null, "default", Locale.ENGLISH)); } @Test @@ -389,34 +400,49 @@ void reloadableResourceBundleMessageSourceFileNameCalculation() { ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); List filenames = ms.calculateFilenamesForLocale("messages", Locale.ENGLISH); - assertThat(filenames).hasSize(1); - assertThat(filenames.get(0)).isEqualTo("messages_en"); + assertThat(filenames).containsExactly("messages_en"); filenames = ms.calculateFilenamesForLocale("messages", Locale.UK); - assertThat(filenames).hasSize(2); - assertThat(filenames.get(1)).isEqualTo("messages_en"); - assertThat(filenames.get(0)).isEqualTo("messages_en_GB"); + assertThat(filenames).containsExactly("messages_en_GB", "messages_en"); filenames = ms.calculateFilenamesForLocale("messages", new Locale("en", "GB", "POSIX")); - assertThat(filenames).hasSize(3); - assertThat(filenames.get(2)).isEqualTo("messages_en"); - assertThat(filenames.get(1)).isEqualTo("messages_en_GB"); - assertThat(filenames.get(0)).isEqualTo("messages_en_GB_POSIX"); + assertThat(filenames).containsExactly("messages_en_GB_POSIX","messages_en_GB", "messages_en"); filenames = ms.calculateFilenamesForLocale("messages", new Locale("en", "", "POSIX")); - assertThat(filenames).hasSize(2); - assertThat(filenames.get(1)).isEqualTo("messages_en"); - assertThat(filenames.get(0)).isEqualTo("messages_en__POSIX"); + assertThat(filenames).containsExactly("messages_en__POSIX", "messages_en"); filenames = ms.calculateFilenamesForLocale("messages", new Locale("", "UK", "POSIX")); - assertThat(filenames).hasSize(2); - assertThat(filenames.get(1)).isEqualTo("messages__UK"); - assertThat(filenames.get(0)).isEqualTo("messages__UK_POSIX"); + assertThat(filenames).containsExactly("messages__UK_POSIX", "messages__UK"); filenames = ms.calculateFilenamesForLocale("messages", new Locale("", "", "POSIX")); assertThat(filenames).isEmpty(); } + @Test + void reloadableResourceBundleMessageSourceWithCustomFileExtensions() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setFileExtensions(List.of(".toskip", ".custom")); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void reloadableResourceBundleMessageSourceWithEmptyCustomFileExtensions() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + assertThatThrownBy(() -> ms.setFileExtensions(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one file extension is required"); + } + + @Test + void reloadableResourceBundleMessageSourceWithInvalidCustomFileExtensions() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + assertThatThrownBy(() -> ms.setFileExtensions(List.of("invalid"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("File extension 'invalid' should start with '.'"); + } + @Test void messageSourceResourceBundle() { ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); diff --git a/spring-context/src/test/java/org/springframework/context/support/SerializableBeanFactoryMemoryLeakTests.java b/spring-context/src/test/java/org/springframework/context/support/SerializableBeanFactoryMemoryLeakTests.java index 45aaac584dd6..6b1d2d6b06e1 100644 --- a/spring-context/src/test/java/org/springframework/context/support/SerializableBeanFactoryMemoryLeakTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/SerializableBeanFactoryMemoryLeakTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ * * @author Chris Beams */ -public class SerializableBeanFactoryMemoryLeakTests { +class SerializableBeanFactoryMemoryLeakTests { /** * Defensively zero-out static factory count - other tests @@ -44,29 +44,29 @@ public class SerializableBeanFactoryMemoryLeakTests { */ @BeforeAll @AfterAll - public static void zeroOutFactoryCount() throws Exception { + static void zeroOutFactoryCount() throws Exception { getSerializableFactoryMap().clear(); } @Test - public void genericContext() throws Exception { + void genericContext() throws Exception { assertFactoryCountThroughoutLifecycle(new GenericApplicationContext()); } @Test - public void abstractRefreshableContext() throws Exception { + void abstractRefreshableContext() throws Exception { assertFactoryCountThroughoutLifecycle(new ClassPathXmlApplicationContext()); } @Test - public void genericContextWithMisconfiguredBean() throws Exception { + void genericContextWithMisconfiguredBean() throws Exception { GenericApplicationContext ctx = new GenericApplicationContext(); registerMisconfiguredBeanDefinition(ctx); assertFactoryCountThroughoutLifecycle(ctx); } @Test - public void abstractRefreshableContextWithMisconfiguredBean() throws Exception { + void abstractRefreshableContextWithMisconfiguredBean() throws Exception { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext() { @Override protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { diff --git a/spring-context/src/test/java/org/springframework/context/support/Service.java b/spring-context/src/test/java/org/springframework/context/support/Service.java index 680e8091ec14..0c177d36fe6d 100644 --- a/spring-context/src/test/java/org/springframework/context/support/Service.java +++ b/spring-context/src/test/java/org/springframework/context/support/Service.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.context.support; +import java.util.Set; + import org.springframework.beans.factory.BeanCreationNotAllowedException; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; @@ -37,6 +39,8 @@ public class Service implements ApplicationContextAware, MessageSourceAware, Dis private Resource[] resources; + private Set resourceSet; + private boolean properlyDestroyed = false; @@ -65,25 +69,30 @@ public Resource[] getResources() { return resources; } + public void setResourceSet(Set resourceSet) { + this.resourceSet = resourceSet; + } + + public Set getResourceSet() { + return resourceSet; + } + @Override public void destroy() { this.properlyDestroyed = true; - Thread thread = new Thread() { - @Override - public void run() { - Assert.state(applicationContext.getBean("messageSource") instanceof StaticMessageSource, - "Invalid MessageSource bean"); - try { - applicationContext.getBean("service2"); - // Should have thrown BeanCreationNotAllowedException - properlyDestroyed = false; - } - catch (BeanCreationNotAllowedException ex) { - // expected - } + Thread thread = new Thread(() -> { + Assert.state(applicationContext.getBean("messageSource") instanceof StaticMessageSource, + "Invalid MessageSource bean"); + try { + applicationContext.getBean("service2"); + // Should have thrown BeanCreationNotAllowedException + properlyDestroyed = false; + } + catch (BeanCreationNotAllowedException ex) { + // expected } - }; + }); thread.start(); try { thread.join(); diff --git a/spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java b/spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java index 3119d051f49f..5693581fe935 100644 --- a/spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ class SimpleThreadScopeTests { @Test - void getFromScope() throws Exception { + void getFromScope() { String name = "removeNodeStatusScreen"; TestBean bean = this.applicationContext.getBean(name, TestBean.class); assertThat(bean).isNotNull(); @@ -47,7 +47,7 @@ void getFromScope() throws Exception { } @Test - void getMultipleInstances() throws Exception { + void getMultipleInstances() { // Arrange TestBean[] beans = new TestBean[2]; Thread thread1 = new Thread(() -> beans[0] = applicationContext.getBean("removeNodeStatusScreen", TestBean.class)); @@ -57,7 +57,7 @@ void getMultipleInstances() throws Exception { thread2.start(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> (beans[0] != null) && (beans[1] != null)); assertThat(beans[1]).isNotSameAs(beans[0]); diff --git a/spring-context/src/test/java/org/springframework/context/support/Spr7283Tests.java b/spring-context/src/test/java/org/springframework/context/support/Spr7283Tests.java index 3251f0c07abe..6a8c9c96c9f7 100644 --- a/spring-context/src/test/java/org/springframework/context/support/Spr7283Tests.java +++ b/spring-context/src/test/java/org/springframework/context/support/Spr7283Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,9 +32,8 @@ class Spr7283Tests { void listWithInconsistentElementTypes() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spr7283.xml", getClass()); List list = ctx.getBean("list", List.class); - assertThat(list).hasSize(2); - assertThat(list.get(0)).isInstanceOf(A.class); - assertThat(list.get(1)).isInstanceOf(B.class); + assertThat(list).satisfiesExactly(zero -> assertThat(zero).isInstanceOf(A.class), + one -> assertThat(one).isInstanceOf(B.class)); ctx.close(); } diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java index a4bff1100c9a..62535c0d2724 100644 --- a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,13 +43,13 @@ * * @author Juergen Hoeller */ -public class StaticApplicationContextMulticasterTests extends AbstractApplicationContextTests { +class StaticApplicationContextMulticasterTests extends AbstractApplicationContextTests { protected StaticApplicationContext sac; @SuppressWarnings("deprecation") @Override - protected ConfigurableApplicationContext createContext() throws Exception { + protected ConfigurableApplicationContext createContext() { StaticApplicationContext parent = new StaticApplicationContext(); Map m = new HashMap<>(); m.put("name", "Roderick"); diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextTests.java index 023efd449bb8..a6e8e9acc114 100644 --- a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,13 +35,13 @@ * * @author Rod Johnson */ -public class StaticApplicationContextTests extends AbstractApplicationContextTests { +class StaticApplicationContextTests extends AbstractApplicationContextTests { protected StaticApplicationContext sac; @SuppressWarnings("deprecation") @Override - protected ConfigurableApplicationContext createContext() throws Exception { + protected ConfigurableApplicationContext createContext() { StaticApplicationContext parent = new StaticApplicationContext(); Map m = new HashMap<>(); m.put("name", "Roderick"); diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java index 8596f602be7e..06a94f73a923 100644 --- a/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class StaticMessageSourceTests extends AbstractApplicationContextTests { +class StaticMessageSourceTests extends AbstractApplicationContextTests { protected static final String MSG_TXT1_US = "At '{1,time}' on \"{1,date}\", there was \"{2}\" on planet {0,number,integer}."; @@ -68,14 +68,14 @@ public void messageSource() throws NoSuchMessageException { } @Test - public void getMessageWithDefaultPassedInAndFoundInMsgCatalog() { + void getMessageWithDefaultPassedInAndFoundInMsgCatalog() { // Try with Locale.US assertThat(sac.getMessage("message.format.example2", null, "This is a default msg if not found in MessageSource.", Locale.US)).as("valid msg from staticMsgSource with default msg passed in returned msg from msg catalog for Locale.US") .isEqualTo("This is a test message in the message catalog with no args."); } @Test - public void getMessageWithDefaultPassedInAndNotFoundInMsgCatalog() { + void getMessageWithDefaultPassedInAndNotFoundInMsgCatalog() { // Try with Locale.US assertThat(sac.getMessage("bogus.message", null, "This is a default msg if not found in MessageSource.", Locale.US)).as("bogus msg from staticMsgSource with default msg passed in returned default msg for Locale.US") .isEqualTo("This is a default msg if not found in MessageSource."); @@ -89,7 +89,7 @@ public void getMessageWithDefaultPassedInAndNotFoundInMsgCatalog() { * @see org.springframework.context.support.AbstractMessageSource for more details. */ @Test - public void getMessageWithMessageAlreadyLookedFor() { + void getMessageWithMessageAlreadyLookedFor() { Object[] arguments = { 7, new Date(System.currentTimeMillis()), "a disturbance in the Force" @@ -117,7 +117,7 @@ public void getMessageWithMessageAlreadyLookedFor() { * Example taken from the javadocs for the java.text.MessageFormat class */ @Test - public void getMessageWithNoDefaultPassedInAndFoundInMsgCatalog() { + void getMessageWithNoDefaultPassedInAndFoundInMsgCatalog() { Object[] arguments = { 7, new Date(System.currentTimeMillis()), "a disturbance in the Force" @@ -144,14 +144,14 @@ public void getMessageWithNoDefaultPassedInAndFoundInMsgCatalog() { } @Test - public void getMessageWithNoDefaultPassedInAndNotFoundInMsgCatalog() { + void getMessageWithNoDefaultPassedInAndNotFoundInMsgCatalog() { // Try with Locale.US assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> sac.getMessage("bogus.message", null, Locale.US)); } @Test - public void messageSourceResolvable() { + void messageSourceResolvable() { // first code valid String[] codes1 = new String[] {"message.format.example3", "message.format.example2"}; MessageSourceResolvable resolvable1 = new DefaultMessageSourceResolvable(codes1, null, "default"); @@ -177,7 +177,7 @@ public void messageSourceResolvable() { @SuppressWarnings("deprecation") @Override - protected ConfigurableApplicationContext createContext() throws Exception { + protected ConfigurableApplicationContext createContext() { StaticApplicationContext parent = new StaticApplicationContext(); Map m = new HashMap<>(); @@ -215,7 +215,7 @@ protected ConfigurableApplicationContext createContext() throws Exception { } @Test - public void nestedMessageSourceWithParamInChild() { + void nestedMessageSourceWithParamInChild() { StaticMessageSource source = new StaticMessageSource(); StaticMessageSource parent = new StaticMessageSource(); source.setParentMessageSource(parent); @@ -230,7 +230,7 @@ public void nestedMessageSourceWithParamInChild() { } @Test - public void nestedMessageSourceWithParamInParent() { + void nestedMessageSourceWithParamInParent() { StaticMessageSource source = new StaticMessageSource(); StaticMessageSource parent = new StaticMessageSource(); source.setParentMessageSource(parent); diff --git a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerEventTests.java b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerEventTests.java index 71692278a41e..1f02174dcb1d 100644 --- a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerEventTests.java +++ b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class JeeNamespaceHandlerEventTests { +class JeeNamespaceHandlerEventTests { private final CollectingReaderEventListener eventListener = new CollectingReaderEventListener(); @@ -43,7 +43,7 @@ public class JeeNamespaceHandlerEventTests { @BeforeEach - public void setup() throws Exception { + void setup() { this.reader = new XmlBeanDefinitionReader(this.beanFactory); this.reader.setEventListener(this.eventListener); this.reader.loadBeanDefinitions(new ClassPathResource("jeeNamespaceHandlerTests.xml", getClass())); @@ -51,21 +51,21 @@ public void setup() throws Exception { @Test - public void testJndiLookupComponentEventReceived() { + void testJndiLookupComponentEventReceived() { ComponentDefinition component = this.eventListener.getComponentDefinition("simple"); boolean condition = component instanceof BeanComponentDefinition; assertThat(condition).isTrue(); } @Test - public void testLocalSlsbComponentEventReceived() { + void testLocalSlsbComponentEventReceived() { ComponentDefinition component = this.eventListener.getComponentDefinition("simpleLocalEjb"); boolean condition = component instanceof BeanComponentDefinition; assertThat(condition).isTrue(); } @Test - public void testRemoteSlsbComponentEventReceived() { + void testRemoteSlsbComponentEventReceived() { ComponentDefinition component = this.eventListener.getComponentDefinition("simpleRemoteEjb"); boolean condition = component instanceof BeanComponentDefinition; assertThat(condition).isTrue(); diff --git a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java index 65292d305730..b2d3777b583d 100644 --- a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,12 @@ package org.springframework.ejb.config; +import javax.naming.NoInitialContextException; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -29,6 +32,7 @@ import org.springframework.jndi.JndiObjectFactoryBean; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Rob Harrop @@ -36,13 +40,13 @@ * @author Chris Beams * @author Oliver Gierke */ -public class JeeNamespaceHandlerTests { +class JeeNamespaceHandlerTests { private ConfigurableListableBeanFactory beanFactory; @BeforeEach - public void setup() { + void setup() { GenericApplicationContext ctx = new GenericApplicationContext(); new XmlBeanDefinitionReader(ctx).loadBeanDefinitions( new ClassPathResource("jeeNamespaceHandlerTests.xml", getClass())); @@ -53,7 +57,7 @@ public void setup() { @Test - public void testSimpleDefinition() { + void testSimpleDefinition() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simple"); assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); assertPropertyValue(beanDefinition, "jndiName", "jdbc/MyDataSource"); @@ -61,7 +65,7 @@ public void testSimpleDefinition() { } @Test - public void testComplexDefinition() { + void testComplexDefinition() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complex"); assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); assertPropertyValue(beanDefinition, "jndiName", "jdbc/MyDataSource"); @@ -75,35 +79,65 @@ public void testComplexDefinition() { } @Test - public void testWithEnvironment() { + void testWithEnvironment() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("withEnvironment"); assertPropertyValue(beanDefinition, "jndiEnvironment", "foo=bar"); assertPropertyValue(beanDefinition, "defaultObject", new RuntimeBeanReference("myBean")); } @Test - public void testWithReferencedEnvironment() { + void testWithReferencedEnvironment() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("withReferencedEnvironment"); assertPropertyValue(beanDefinition, "jndiEnvironment", new RuntimeBeanReference("myEnvironment")); assertThat(beanDefinition.getPropertyValues().contains("environmentRef")).isFalse(); } @Test - public void testSimpleLocalSlsb() { + void testSimpleLocalSlsb() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simpleLocalEjb"); assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); assertPropertyValue(beanDefinition, "jndiName", "ejb/MyLocalBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("simpleLocalEjb")) + .withCauseInstanceOf(NoInitialContextException.class); } @Test - public void testSimpleRemoteSlsb() { + void testSimpleRemoteSlsb() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simpleRemoteEjb"); assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); assertPropertyValue(beanDefinition, "jndiName", "ejb/MyRemoteBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("simpleRemoteEjb")) + .withCauseInstanceOf(NoInitialContextException.class); + } + + @Test + void testComplexLocalSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complexLocalEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyLocalBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("complexLocalEjb")) + .withCauseInstanceOf(NoInitialContextException.class); + } + + @Test + void testComplexRemoteSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complexRemoteEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyRemoteBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("complexRemoteEjb")) + .withCauseInstanceOf(NoInitialContextException.class); } @Test - public void testLazyInitJndiLookup() { + void testLazyInitJndiLookup() { BeanDefinition definition = this.beanFactory.getMergedBeanDefinition("lazyDataSource"); assertThat(definition.isLazyInit()).isTrue(); definition = this.beanFactory.getMergedBeanDefinition("lazyLocalBean"); diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java index d085e4d622fa..f74f36edbd95 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,75 +35,83 @@ * * @author Keith Donald * @author Phillip Webb + * @author Juergen Hoeller */ -public class DateFormatterTests { +class DateFormatterTests { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); @Test - public void shouldPrintAndParseDefault() throws Exception { + void shouldPrintAndParseDefault() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseFromPattern() throws ParseException { + void shouldPrintAndParseFromPattern() throws ParseException { DateFormatter formatter = new DateFormatter("yyyy-MM-dd"); formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseShort() throws Exception { + void shouldPrintAndParseShort() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.SHORT); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09"); assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseMedium() throws Exception { + void shouldPrintAndParseMedium() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.MEDIUM); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseLong() throws Exception { + void shouldPrintAndParseLong() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.LONG); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009"); assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseFull() throws Exception { + void shouldPrintAndParseFull() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.FULL); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009"); assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseISODate() throws Exception { + void shouldPrintAndParseIsoDate() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.DATE); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); assertThat(formatter.parse("2009-6-01", Locale.US)) @@ -111,45 +119,56 @@ public void shouldPrintAndParseISODate() throws Exception { } @Test - public void shouldPrintAndParseISOTime() throws Exception { + void shouldPrintAndParseIsoTime() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.TIME); + Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z"); assertThat(formatter.parse("14:23:05.003Z", Locale.US)) .isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3)); + + date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0); + assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z"); + assertThat(formatter.parse("14:23:05Z", Locale.US)) + .isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant()); } @Test - public void shouldPrintAndParseISODateTime() throws Exception { + void shouldPrintAndParseIsoDateTime() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.DATE_TIME); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z"); assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date); + + date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0); + assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z"); + assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant()); } @Test - public void shouldThrowOnUnsupportedStylePattern() throws Exception { + void shouldThrowOnUnsupportedStylePattern() { DateFormatter formatter = new DateFormatter(); formatter.setStylePattern("OO"); - assertThatIllegalStateException().isThrownBy(() -> - formatter.parse("2009", Locale.US)) - .withMessageContaining("Unsupported style pattern 'OO'"); + + assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US)) + .withMessageContaining("Unsupported style pattern 'OO'"); } @Test - public void shouldUseCorrectOrder() throws Exception { + void shouldUseCorrectOrder() { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.SHORT); formatter.setStylePattern("L-"); formatter.setIso(ISO.DATE_TIME); formatter.setPattern("yyyy"); - Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009"); formatter.setPattern(""); diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index 2ea58f5e06a7..b45a4a28c2c1 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class DateFormattingTests { +class DateFormattingTests { private final FormattingConversionService conversionService = new FormattingConversionService(); @@ -61,12 +61,11 @@ public class DateFormattingTests { @BeforeEach void setup() { - DateFormatterRegistrar registrar = new DateFormatterRegistrar(); - setup(registrar); + DefaultConversionService.addDefaultConverters(conversionService); + setup(new DateFormatterRegistrar()); } private void setup(DateFormatterRegistrar registrar) { - DefaultConversionService.addDefaultConverters(conversionService); registrar.registerFormatters(conversionService); SimpleDateBean bean = new SimpleDateBean(); @@ -172,7 +171,7 @@ void testBindDateAnnotatedWithError() { @Test @Disabled void testBindDateAnnotatedWithFallbackError() { - // TODO This currently passes because of the Date(String) constructor fallback is used + // TODO This currently passes because the Date(String) constructor fallback is used MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("styleDate", "Oct 031, 2009"); binder.bind(propertyValues); @@ -181,7 +180,7 @@ void testBindDateAnnotatedWithFallbackError() { } @Test - void testBindDateAnnotatedPattern() { + void testBindDateTimePatternAnnotated() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("patternDate", "10/31/09 1:05"); binder.bind(propertyValues); @@ -190,7 +189,7 @@ void testBindDateAnnotatedPattern() { } @Test - void testBindDateAnnotatedPatternWithGlobalFormat() { + void testBindDateTimePatternAnnotatedWithGlobalFormat() { DateFormatterRegistrar registrar = new DateFormatterRegistrar(); DateFormatter dateFormatter = new DateFormatter(); dateFormatter.setIso(ISO.DATE_TIME); @@ -205,7 +204,7 @@ void testBindDateAnnotatedPatternWithGlobalFormat() { } @Test - void testBindDateTimeOverflow() { + void testBindDateTimePatternAnnotatedWithOverflow() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("patternDate", "02/29/09 12:00 PM"); binder.bind(propertyValues); diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java index eb3b3ea126d1..fc2277bee8be 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,37 +23,33 @@ import static org.assertj.core.api.Assertions.assertThat; - - - - /** * @author Phillip Webb * @author Sam Brannen */ -public class DateTimeFormatterFactoryBeanTests { +class DateTimeFormatterFactoryBeanTests { private final DateTimeFormatterFactoryBean factory = new DateTimeFormatterFactoryBean(); @Test - public void isSingleton() { + void isSingleton() { assertThat(factory.isSingleton()).isTrue(); } @Test - public void getObjectType() { + void getObjectType() { assertThat(factory.getObjectType()).isEqualTo(DateTimeFormatter.class); } @Test - public void getObject() { + void getObject() { factory.afterPropertiesSet(); assertThat(factory.getObject().toString()).isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()); } @Test - public void getObjectIsAlwaysSingleton() { + void getObjectIsAlwaysSingleton() { factory.afterPropertiesSet(); DateTimeFormatter formatter = factory.getObject(); assertThat(formatter.toString()).isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()); diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java index 081aa02dff1f..8f1a9cff3cf6 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,14 +34,14 @@ * @author Phillip Webb * @author Sam Brannen */ -public class DateTimeFormatterFactoryTests { +class DateTimeFormatterFactoryTests { // Potential test timezone, both have daylight savings on October 21st private static final TimeZone ZURICH = TimeZone.getTimeZone("Europe/Zurich"); private static final TimeZone NEW_YORK = TimeZone.getTimeZone("America/New_York"); // Ensure that we are testing against a timezone other than the default. - private static final TimeZone TEST_TIMEZONE = ZURICH.equals(TimeZone.getDefault()) ? NEW_YORK : ZURICH; + private static final TimeZone TEST_TIMEZONE = (ZURICH.equals(TimeZone.getDefault()) ? NEW_YORK : ZURICH); private DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); @@ -50,36 +50,36 @@ public class DateTimeFormatterFactoryTests { @Test - public void createDateTimeFormatter() { + void createDateTimeFormatter() { assertThat(factory.createDateTimeFormatter().toString()).isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()); } @Test - public void createDateTimeFormatterWithPattern() { + void createDateTimeFormatterWithPattern() { factory = new DateTimeFormatterFactory("yyyyMMddHHmmss"); DateTimeFormatter formatter = factory.createDateTimeFormatter(); assertThat(formatter.format(dateTime)).isEqualTo("20091021121000"); } @Test - public void createDateTimeFormatterWithNullFallback() { + void createDateTimeFormatterWithNullFallback() { DateTimeFormatter formatter = factory.createDateTimeFormatter(null); assertThat(formatter).isNull(); } @Test - public void createDateTimeFormatterWithFallback() { + void createDateTimeFormatterWithFallback() { DateTimeFormatter fallback = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG); DateTimeFormatter formatter = factory.createDateTimeFormatter(fallback); assertThat(formatter).isSameAs(fallback); } @Test - public void createDateTimeFormatterInOrderOfPropertyPriority() { + void createDateTimeFormatterInOrderOfPropertyPriority() { factory.setStylePattern("SS"); String value = applyLocale(factory.createDateTimeFormatter()).format(dateTime); - assertThat(value).startsWith("10/21/09"); - assertThat(value).endsWith("12:10 PM"); + // \p{Zs} matches any Unicode space character + assertThat(value).startsWith("10/21/09").matches(".+?12:10\\p{Zs}PM"); factory.setIso(ISO.DATE); assertThat(applyLocale(factory.createDateTimeFormatter()).format(dateTime)).isEqualTo("2009-10-21"); @@ -89,7 +89,7 @@ public void createDateTimeFormatterInOrderOfPropertyPriority() { } @Test - public void createDateTimeFormatterWithTimeZone() { + void createDateTimeFormatterWithTimeZone() { factory.setPattern("yyyyMMddHHmmss Z"); factory.setTimeZone(TEST_TIMEZONE); ZoneId dateTimeZone = TEST_TIMEZONE.toZoneId(); diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 392fdd61c6e5..43a8f1c13e47 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -58,6 +59,8 @@ import org.springframework.validation.FieldError; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.condition.JRE.JAVA_19; +import static org.junit.jupiter.api.condition.JRE.JAVA_20; /** * @author Keith Donald @@ -68,6 +71,12 @@ */ class DateTimeFormattingTests { + // JDK <= 19 requires a standard space before "AM/PM". + // JDK >= 20 requires a NNBSP before "AM/PM". + // \u202F is a narrow non-breaking space (NNBSP). + private static final String TIME_SEPARATOR = (Runtime.version().feature() < 20 ? " " : "\u202F"); + + private final FormattingConversionService conversionService = new FormattingConversionService(); private DataBinder binder; @@ -210,10 +219,11 @@ void testBindLocalDateFromJavaUtilCalendar() { @Test void testBindLocalTime() { MutablePropertyValues propertyValues = new MutablePropertyValues(); - propertyValues.add("localTime", "12:00 PM"); + propertyValues.add("localTime", "12:00%sPM".formatted(TIME_SEPARATOR)); binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); - assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00\\p{Zs}PM"); } @Test @@ -222,7 +232,8 @@ void testBindLocalTimeWithISO() { propertyValues.add("localTime", "12:00:00"); binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); - assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00\\p{Zs}PM"); } @Test @@ -231,10 +242,11 @@ void testBindLocalTimeWithSpecificStyle() { registrar.setTimeStyle(FormatStyle.MEDIUM); setup(registrar); MutablePropertyValues propertyValues = new MutablePropertyValues(); - propertyValues.add("localTime", "12:00:00 PM"); + propertyValues.add("localTime", "12:00:00%sPM".formatted(TIME_SEPARATOR)); binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); - assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00:00\\p{Zs}PM"); } @Test @@ -252,10 +264,11 @@ void testBindLocalTimeWithSpecificFormatter() { @Test void testBindLocalTimeAnnotated() { MutablePropertyValues propertyValues = new MutablePropertyValues(); - propertyValues.add("styleLocalTime", "12:00:00 PM"); + propertyValues.add("styleLocalTime", "12:00:00%sPM".formatted(TIME_SEPARATOR)); binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); - assertThat(binder.getBindingResult().getFieldValue("styleLocalTime")).isEqualTo("12:00:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(binder.getBindingResult().getFieldValue("styleLocalTime")).asString().matches("12:00:00\\p{Zs}PM"); } @Test @@ -264,7 +277,8 @@ void testBindLocalTimeFromJavaUtilCalendar() { propertyValues.add("localTime", new GregorianCalendar(1970, 0, 0, 12, 0)); binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); - assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00\\p{Zs}PM"); } @Test @@ -274,9 +288,8 @@ void testBindLocalDateTime() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("10/31/09") - .endsWith("12:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM"); } @Test @@ -286,9 +299,8 @@ void testBindLocalDateTimeWithISO() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("10/31/09") - .endsWith("12:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM"); } @Test @@ -298,9 +310,8 @@ void testBindLocalDateTimeAnnotated() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("styleLocalDateTime").toString(); - assertThat(value) - .startsWith("Oct 31, 2009") - .endsWith("12:00:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(value).startsWith("Oct 31, 2009").matches(".+?12:00:00\\p{Zs}PM"); } @Test @@ -310,9 +321,8 @@ void testBindLocalDateTimeFromJavaUtilCalendar() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("10/31/09") - .endsWith("12:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM"); } @Test @@ -325,9 +335,8 @@ void testBindDateTimeWithSpecificStyle() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("Oct 31, 2009") - .endsWith("12:00:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(value).startsWith("Oct 31, 2009").matches(".+?12:00:00\\p{Zs}PM"); } @Test @@ -512,7 +521,7 @@ void testBindYearMonth() { } @Test - public void testBindYearMonthAnnotatedPattern() { + void testBindYearMonthAnnotatedPattern() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("yearMonthAnnotatedPattern", "12/2007"); binder.bind(propertyValues); @@ -531,7 +540,7 @@ void testBindMonthDay() { } @Test - public void testBindMonthDayAnnotatedPattern() { + void testBindMonthDayAnnotatedPattern() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("monthDayAnnotatedPattern", "1/3"); binder.bind(propertyValues); @@ -540,6 +549,7 @@ public void testBindMonthDayAnnotatedPattern() { assertThat(binder.getBindingResult().getRawFieldValue("monthDayAnnotatedPattern")).isEqualTo(MonthDay.parse("--01-03")); } + @Nested class FallbackPatternTests { @@ -567,18 +577,32 @@ void patternLocalDate(String propertyValue) { assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02"); } + @EnabledForJreRange(max = JAVA_19) @ParameterizedTest(name = "input date: {0}") - // @ValueSource(strings = {"12:00:00\u202FPM", "12:00:00", "12:00"}) + // JDK <= 19 requires a standard space before the "PM". @ValueSource(strings = {"12:00:00 PM", "12:00:00", "12:00"}) - void styleLocalTime(String propertyValue) { + void styleLocalTime_PreJDK20(String propertyValue) { + styleLocalTime(propertyValue); + } + + @EnabledForJreRange(min = JAVA_20) + @ParameterizedTest(name = "input date: {0}") + // JDK >= 20 requires a NNBSP before the "PM". + // \u202F is a narrow non-breaking space (NNBSP). + @ValueSource(strings = {"12:00:00\u202FPM", "12:00:00", "12:00"}) + void styleLocalTime_PostJDK20(String propertyValue) { + styleLocalTime(propertyValue); + } + + private void styleLocalTime(String propertyValue) { String propertyName = "styleLocalTimeWithFallbackPatterns"; MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add(propertyName, propertyValue); binder.bind(propertyValues); BindingResult bindingResult = binder.getBindingResult(); assertThat(bindingResult.getErrorCount()).isZero(); - // assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\SPM"); - assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("12:00:00 PM"); + // \p{Zs} matches any Unicode space character + assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\p{Zs}PM"); } @ParameterizedTest(name = "input date: {0}") @@ -621,6 +645,19 @@ void patternLocalDateWithUnsupportedPattern() { .hasMessageStartingWith("Text '210302'") .hasNoCause(); } + + @Test + void testBindInstantAsLongEpochMillis() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("instant", 1234L); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getRawFieldValue("instant")) + .isInstanceOf(Instant.class) + .isEqualTo(Instant.ofEpochMilli(1234L)); + assertThat(binder.getBindingResult().getFieldValue("instant")) + .hasToString("1970-01-01T00:00:01.234Z"); + } } @@ -631,10 +668,10 @@ public static class DateTimeBean { @DateTimeFormat(style = "M-") private LocalDate styleLocalDate; - @DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" }) + @DateTimeFormat(style = "S-", fallbackPatterns = {"yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd"}) private LocalDate styleLocalDateWithFallbackPatterns; - @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" }) + @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = {"M/d/yy", "yyyyMMdd", "yyyy.MM.dd"}) private LocalDate patternLocalDateWithFallbackPatterns; private LocalTime localTime; @@ -642,7 +679,7 @@ public static class DateTimeBean { @DateTimeFormat(style = "-M") private LocalTime styleLocalTime; - @DateTimeFormat(style = "-M", fallbackPatterns = { "HH:mm:ss", "HH:mm"}) + @DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"}) private LocalTime styleLocalTimeWithFallbackPatterns; private LocalDateTime localDateTime; @@ -662,7 +699,7 @@ public static class DateTimeBean { @DateTimeFormat(iso = ISO.DATE_TIME) private LocalDateTime isoLocalDateTime; - @DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = { "yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"}) + @DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = {"yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"}) private LocalDateTime isoLocalDateTimeWithFallbackPatterns; private Instant instant; @@ -690,7 +727,6 @@ public static class DateTimeBean { private final List children = new ArrayList<>(); - public LocalDate getLocalDate() { return this.localDate; } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java index 16ba2cbd5e5c..c57bc66bac59 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.text.ParseException; import java.time.Instant; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Locale; import java.util.Random; import java.util.stream.Stream; @@ -37,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link InstantFormatter}. + * Tests for {@link InstantFormatter}. * * @author Andrei Nevedomskii * @author Sam Brannen @@ -49,13 +51,12 @@ class InstantFormatterTests { private final InstantFormatter instantFormatter = new InstantFormatter(); + @ParameterizedTest @ArgumentsSource(ISOSerializedInstantProvider.class) void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); - - Instant actual = instantFormatter.parse(input, null); - + Instant actual = instantFormatter.parse(input, Locale.US); assertThat(actual).isEqualTo(expected); } @@ -63,9 +64,7 @@ void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String in @ArgumentsSource(RFC1123SerializedInstantProvider.class) void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); - - Instant actual = instantFormatter.parse(input, null); - + Instant actual = instantFormatter.parse(input, Locale.US); assertThat(actual).isEqualTo(expected); } @@ -73,12 +72,18 @@ void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(Strin @ArgumentsSource(RandomInstantProvider.class) void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) { String expected = DateTimeFormatter.ISO_INSTANT.format(input); - - String actual = instantFormatter.print(input, null); - + String actual = instantFormatter.print(input, Locale.US); assertThat(actual).isEqualTo(expected); } + @ParameterizedTest + @ArgumentsSource(RandomEpochMillisProvider.class) + void should_parse_into_an_Instant_from_epoch_milli(Instant input) throws ParseException { + Instant actual = instantFormatter.parse(Long.toString(input.toEpochMilli()), Locale.US); + assertThat(actual).isEqualTo(input); + } + + private static class RandomInstantProvider implements ArgumentsProvider { private static final long DATA_SET_SIZE = 10; @@ -100,6 +105,7 @@ Stream randomInstantStream(Instant min, Instant max) { } } + private static class ISOSerializedInstantProvider extends RandomInstantProvider { @Override @@ -108,6 +114,7 @@ Stream provideArguments() { } } + private static class RFC1123SerializedInstantProvider extends RandomInstantProvider { // RFC-1123 supports only 4-digit years @@ -122,4 +129,20 @@ Stream provideArguments() { } } + + private static final class RandomEpochMillisProvider implements ArgumentsProvider { + + private static final long DATA_SET_SIZE = 10; + + private static final Random random = new Random(); + + @Override + public Stream provideArguments(ExtensionContext context) { + return random.longs(DATA_SET_SIZE, Long.MIN_VALUE, Long.MAX_VALUE) + .mapToObj(Instant::ofEpochMilli) + .map(instant -> instant.truncatedTo(ChronoUnit.MILLIS)) + .map(Arguments::of); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/format/number/CurrencyStyleFormatterTests.java b/spring-context/src/test/java/org/springframework/format/number/CurrencyStyleFormatterTests.java index 2a8feb879ea3..d6f5294e2f3c 100644 --- a/spring-context/src/test/java/org/springframework/format/number/CurrencyStyleFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/number/CurrencyStyleFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,40 +29,40 @@ /** * @author Keith Donald */ -public class CurrencyStyleFormatterTests { +class CurrencyStyleFormatterTests { private final CurrencyStyleFormatter formatter = new CurrencyStyleFormatter(); @Test - public void formatValue() { + void formatValue() { assertThat(formatter.print(new BigDecimal("23"), Locale.US)).isEqualTo("$23.00"); } @Test - public void parseValue() throws ParseException { + void parseValue() throws ParseException { assertThat(formatter.parse("$23.56", Locale.US)).isEqualTo(new BigDecimal("23.56")); } @Test - public void parseBogusValue() throws ParseException { + void parseBogusValue() { assertThatExceptionOfType(ParseException.class).isThrownBy(() -> formatter.parse("bogus", Locale.US)); } @Test - public void parseValueDefaultRoundDown() throws ParseException { + void parseValueDefaultRoundDown() throws ParseException { this.formatter.setRoundingMode(RoundingMode.DOWN); assertThat(formatter.parse("$23.567", Locale.US)).isEqualTo(new BigDecimal("23.56")); } @Test - public void parseWholeValue() throws ParseException { + void parseWholeValue() throws ParseException { assertThat(formatter.parse("$23", Locale.US)).isEqualTo(new BigDecimal("23.00")); } @Test - public void parseValueNotLenientFailure() throws ParseException { + void parseValueNotLenientFailure() { assertThatExceptionOfType(ParseException.class).isThrownBy(() -> formatter.parse("$23.56bogus", Locale.US)); } diff --git a/spring-context/src/test/java/org/springframework/format/number/NumberFormattingTests.java b/spring-context/src/test/java/org/springframework/format/number/NumberFormattingTests.java index 609927bdf05e..a65d6dc9dcd6 100644 --- a/spring-context/src/test/java/org/springframework/format/number/NumberFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/number/NumberFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ * @author Keith Donald * @author Juergen Hoeller */ -public class NumberFormattingTests { +class NumberFormattingTests { private final FormattingConversionService conversionService = new FormattingConversionService(); @@ -46,7 +46,7 @@ public class NumberFormattingTests { @BeforeEach - public void setUp() { + void setUp() { DefaultConversionService.addDefaultConverters(conversionService); conversionService.setEmbeddedValueResolver(strVal -> { if ("${pattern}".equals(strVal)) { @@ -64,13 +64,13 @@ public void setUp() { } @AfterEach - public void tearDown() { + void tearDown() { LocaleContextHolder.setLocale(null); } @Test - public void testDefaultNumberFormatting() { + void testDefaultNumberFormatting() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("numberDefault", "3,339.12"); binder.bind(propertyValues); @@ -79,7 +79,7 @@ public void testDefaultNumberFormatting() { } @Test - public void testDefaultNumberFormattingAnnotated() { + void testDefaultNumberFormattingAnnotated() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("numberDefaultAnnotated", "3,339.12"); binder.bind(propertyValues); @@ -88,7 +88,7 @@ public void testDefaultNumberFormattingAnnotated() { } @Test - public void testCurrencyFormatting() { + void testCurrencyFormatting() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("currency", "$3,339.12"); binder.bind(propertyValues); @@ -97,7 +97,7 @@ public void testCurrencyFormatting() { } @Test - public void testPercentFormatting() { + void testPercentFormatting() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("percent", "53%"); binder.bind(propertyValues); @@ -106,7 +106,7 @@ public void testPercentFormatting() { } @Test - public void testPatternFormatting() { + void testPatternFormatting() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("pattern", "1,25.00"); binder.bind(propertyValues); @@ -115,7 +115,7 @@ public void testPatternFormatting() { } @Test - public void testPatternArrayFormatting() { + void testPatternArrayFormatting() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("patternArray", new String[] { "1,25.00", "2,35.00" }); binder.bind(propertyValues); @@ -133,7 +133,7 @@ public void testPatternArrayFormatting() { } @Test - public void testPatternListFormatting() { + void testPatternListFormatting() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("patternList", new String[] { "1,25.00", "2,35.00" }); binder.bind(propertyValues); @@ -151,7 +151,7 @@ public void testPatternListFormatting() { } @Test - public void testPatternList2FormattingListElement() { + void testPatternList2FormattingListElement() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("patternList2[0]", "1,25.00"); propertyValues.add("patternList2[1]", "2,35.00"); @@ -162,7 +162,7 @@ public void testPatternList2FormattingListElement() { } @Test - public void testPatternList2FormattingList() { + void testPatternList2FormattingList() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("patternList2[0]", "1,25.00"); propertyValues.add("patternList2[1]", "2,35.00"); diff --git a/spring-context/src/test/java/org/springframework/format/number/NumberStyleFormatterTests.java b/spring-context/src/test/java/org/springframework/format/number/NumberStyleFormatterTests.java index babd8a3267d1..ff535886038b 100644 --- a/spring-context/src/test/java/org/springframework/format/number/NumberStyleFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/number/NumberStyleFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,29 +28,29 @@ /** * @author Keith Donald */ -public class NumberStyleFormatterTests { +class NumberStyleFormatterTests { private final NumberStyleFormatter formatter = new NumberStyleFormatter(); @Test - public void formatValue() { + void formatValue() { assertThat(formatter.print(new BigDecimal("23.56"), Locale.US)).isEqualTo("23.56"); } @Test - public void parseValue() throws ParseException { + void parseValue() throws ParseException { assertThat(formatter.parse("23.56", Locale.US)).isEqualTo(new BigDecimal("23.56")); } @Test - public void parseBogusValue() throws ParseException { + void parseBogusValue() { assertThatExceptionOfType(ParseException.class).isThrownBy(() -> formatter.parse("bogus", Locale.US)); } @Test - public void parsePercentValueNotLenientFailure() throws ParseException { + void parsePercentValueNotLenientFailure() { assertThatExceptionOfType(ParseException.class).isThrownBy(() -> formatter.parse("23.56bogus", Locale.US)); } diff --git a/spring-context/src/test/java/org/springframework/format/number/PercentStyleFormatterTests.java b/spring-context/src/test/java/org/springframework/format/number/PercentStyleFormatterTests.java index de9cbf227e33..0fb1855b9b0e 100644 --- a/spring-context/src/test/java/org/springframework/format/number/PercentStyleFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/number/PercentStyleFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,29 +28,29 @@ /** * @author Keith Donald */ -public class PercentStyleFormatterTests { +class PercentStyleFormatterTests { private final PercentStyleFormatter formatter = new PercentStyleFormatter(); @Test - public void formatValue() { + void formatValue() { assertThat(formatter.print(new BigDecimal(".23"), Locale.US)).isEqualTo("23%"); } @Test - public void parseValue() throws ParseException { + void parseValue() throws ParseException { assertThat(formatter.parse("23.56%", Locale.US)).isEqualTo(new BigDecimal(".2356")); } @Test - public void parseBogusValue() throws ParseException { + void parseBogusValue() { assertThatExceptionOfType(ParseException.class).isThrownBy(() -> formatter.parse("bogus", Locale.US)); } @Test - public void parsePercentValueNotLenientFailure() throws ParseException { + void parsePercentValueNotLenientFailure() { assertThatExceptionOfType(ParseException.class).isThrownBy(() -> formatter.parse("23.56%bogus", Locale.US)); } diff --git a/spring-context/src/test/java/org/springframework/format/number/money/MoneyFormattingTests.java b/spring-context/src/test/java/org/springframework/format/number/money/MoneyFormattingTests.java index c18a9f04df63..348171c77e3c 100644 --- a/spring-context/src/test/java/org/springframework/format/number/money/MoneyFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/number/money/MoneyFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,24 +38,24 @@ * @author Juergen Hoeller * @since 4.2 */ -public class MoneyFormattingTests { +class MoneyFormattingTests { private final FormattingConversionService conversionService = new DefaultFormattingConversionService(); @BeforeEach - public void setUp() { + void setUp() { LocaleContextHolder.setLocale(Locale.US); } @AfterEach - public void tearDown() { + void tearDown() { LocaleContextHolder.setLocale(null); } @Test - public void testAmountAndUnit() { + void testAmountAndUnit() { MoneyHolder bean = new MoneyHolder(); DataBinder binder = new DataBinder(bean); binder.setConversionService(conversionService); @@ -81,7 +81,7 @@ public void testAmountAndUnit() { } @Test - public void testAmountWithNumberFormat1() { + void testAmountWithNumberFormat1() { FormattedMoneyHolder1 bean = new FormattedMoneyHolder1(); DataBinder binder = new DataBinder(bean); binder.setConversionService(conversionService); @@ -104,7 +104,7 @@ public void testAmountWithNumberFormat1() { } @Test - public void testAmountWithNumberFormat2() { + void testAmountWithNumberFormat2() { FormattedMoneyHolder2 bean = new FormattedMoneyHolder2(); DataBinder binder = new DataBinder(bean); binder.setConversionService(conversionService); @@ -119,7 +119,7 @@ public void testAmountWithNumberFormat2() { } @Test - public void testAmountWithNumberFormat3() { + void testAmountWithNumberFormat3() { FormattedMoneyHolder3 bean = new FormattedMoneyHolder3(); DataBinder binder = new DataBinder(bean); binder.setConversionService(conversionService); @@ -134,7 +134,7 @@ public void testAmountWithNumberFormat3() { } @Test - public void testAmountWithNumberFormat4() { + void testAmountWithNumberFormat4() { FormattedMoneyHolder4 bean = new FormattedMoneyHolder4(); DataBinder binder = new DataBinder(bean); binder.setConversionService(conversionService); @@ -149,7 +149,7 @@ public void testAmountWithNumberFormat4() { } @Test - public void testAmountWithNumberFormat5() { + void testAmountWithNumberFormat5() { FormattedMoneyHolder5 bean = new FormattedMoneyHolder5(); DataBinder binder = new DataBinder(bean); binder.setConversionService(conversionService); diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java index 160c0fc4f02f..8f473be3c244 100644 --- a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.text.ParseException; import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -47,10 +46,10 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller */ -public class FormattingConversionServiceFactoryBeanTests { +class FormattingConversionServiceFactoryBeanTests { @Test - public void testDefaultFormattersOn() throws Exception { + void testDefaultFormattersOn() throws Exception { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); factory.afterPropertiesSet(); FormattingConversionService fcs = factory.getObject(); @@ -69,7 +68,7 @@ public void testDefaultFormattersOn() throws Exception { } @Test - public void testDefaultFormattersOff() throws Exception { + void testDefaultFormattersOff() throws Exception { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); factory.setRegisterDefaultFormatters(false); factory.afterPropertiesSet(); @@ -82,7 +81,7 @@ public void testDefaultFormattersOff() throws Exception { } @Test - public void testCustomFormatter() throws Exception { + void testCustomFormatter() throws Exception { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); Set formatters = new HashSet<>(); formatters.add(new TestBeanFormatter()); @@ -103,7 +102,7 @@ public void testCustomFormatter() throws Exception { } @Test - public void testFormatterRegistrar() throws Exception { + void testFormatterRegistrar() { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); Set registrars = new HashSet<>(); registrars.add(new TestFormatterRegistrar()); @@ -117,7 +116,7 @@ public void testFormatterRegistrar() throws Exception { } @Test - public void testInvalidFormatter() throws Exception { + void testInvalidFormatter() { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); Set formatters = new HashSet<>(); formatters.add(new Object()); @@ -164,7 +163,7 @@ public String print(TestBean object, Locale locale) { } @Override - public TestBean parse(String text, Locale locale) throws ParseException { + public TestBean parse(String text, Locale locale) { TestBean object = new TestBean(); object.setSpecialInt(Integer.parseInt(text)); return object; @@ -189,7 +188,7 @@ public Set> getFieldTypes() { public Printer getPrinter(SpecialInt annotation, Class fieldType) { assertThat(annotation.value()).isEqualTo("aliased"); assertThat(annotation.alias()).isEqualTo("aliased"); - return (object, locale) -> ":" + object.toString(); + return (object, locale) -> ":" + object; } @Override diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceRuntimeHintsTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceRuntimeHintsTests.java new file mode 100644 index 000000000000..50b201414db8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceRuntimeHintsTests.java @@ -0,0 +1,51 @@ +/* + * 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.format.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FormattingConversionServiceRuntimeHints}. + * @author Brian Clozel + */ +class FormattingConversionServiceRuntimeHintsTests { + + private RuntimeHints hints; + + @BeforeEach + void setup() { + this.hints = new RuntimeHints(); + SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories") + .load(RuntimeHintsRegistrar.class).forEach(registrar -> registrar + .registerHints(this.hints, ClassUtils.getDefaultClassLoader())); + } + + @Test + void monetaryAmountHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(javax.money.MonetaryAmount.class)).accepts(this.hints); + } + +} diff --git a/spring-context/src/test/java/org/springframework/instrument/classloading/InstrumentableClassLoaderTests.java b/spring-context/src/test/java/org/springframework/instrument/classloading/InstrumentableClassLoaderTests.java index b70628ea4e02..c896a92d9432 100644 --- a/spring-context/src/test/java/org/springframework/instrument/classloading/InstrumentableClassLoaderTests.java +++ b/spring-context/src/test/java/org/springframework/instrument/classloading/InstrumentableClassLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class InstrumentableClassLoaderTests { +class InstrumentableClassLoaderTests { @Test - public void testDefaultLoadTimeWeaver() { + void testDefaultLoadTimeWeaver() { ClassLoader loader = new SimpleInstrumentableClassLoader(ClassUtils.getDefaultClassLoader()); ReflectiveLoadTimeWeaver handler = new ReflectiveLoadTimeWeaver(loader); assertThat(handler.getInstrumentableClassLoader()).isSameAs(loader); diff --git a/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java b/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java index 989b1e196de2..df738ec9999a 100644 --- a/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java +++ b/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,27 +26,27 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for the {@link ReflectiveLoadTimeWeaver} class. + * Tests for {@link ReflectiveLoadTimeWeaver}. * * @author Rick Evans * @author Chris Beams */ -public class ReflectiveLoadTimeWeaverTests { +class ReflectiveLoadTimeWeaverTests { @Test - public void testCtorWithNullClassLoader() { + void testCtorWithNullClassLoader() { assertThatIllegalArgumentException().isThrownBy(() -> new ReflectiveLoadTimeWeaver(null)); } @Test - public void testCtorWithClassLoaderThatDoesNotExposeAnAddTransformerMethod() { + void testCtorWithClassLoaderThatDoesNotExposeAnAddTransformerMethod() { assertThatIllegalStateException().isThrownBy(() -> new ReflectiveLoadTimeWeaver(getClass().getClassLoader())); } @Test - public void testCtorWithClassLoaderThatDoesNotExposeAGetThrowawayClassLoaderMethodIsOkay() { + void testCtorWithClassLoaderThatDoesNotExposeAGetThrowawayClassLoaderMethodIsOkay() { JustAddTransformerClassLoader classLoader = new JustAddTransformerClassLoader(); ReflectiveLoadTimeWeaver weaver = new ReflectiveLoadTimeWeaver(classLoader); weaver.addTransformer(new ClassFileTransformer() { @@ -59,20 +59,20 @@ public byte[] transform(ClassLoader loader, String className, Class classBein } @Test - public void testAddTransformerWithNullTransformer() { + void testAddTransformerWithNullTransformer() { assertThatIllegalArgumentException().isThrownBy(() -> new ReflectiveLoadTimeWeaver(new JustAddTransformerClassLoader()).addTransformer(null)); } @Test - public void testGetThrowawayClassLoaderWithClassLoaderThatDoesNotExposeAGetThrowawayClassLoaderMethodYieldsFallbackClassLoader() { + void testGetThrowawayClassLoaderWithClassLoaderThatDoesNotExposeAGetThrowawayClassLoaderMethodYieldsFallbackClassLoader() { ReflectiveLoadTimeWeaver weaver = new ReflectiveLoadTimeWeaver(new JustAddTransformerClassLoader()); ClassLoader throwawayClassLoader = weaver.getThrowawayClassLoader(); assertThat(throwawayClassLoader).isNotNull(); } @Test - public void testGetThrowawayClassLoaderWithTotallyCompliantClassLoader() { + void testGetThrowawayClassLoaderWithTotallyCompliantClassLoader() { TotallyCompliantClassLoader classLoader = new TotallyCompliantClassLoader(); ReflectiveLoadTimeWeaver weaver = new ReflectiveLoadTimeWeaver(classLoader); ClassLoader throwawayClassLoader = weaver.getThrowawayClassLoader(); diff --git a/spring-context/src/test/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoaderTests.java b/spring-context/src/test/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoaderTests.java index 86d5a692450e..f464a60f3951 100644 --- a/spring-context/src/test/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoaderTests.java +++ b/spring-context/src/test/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ * @author Chris Beams * @since 2.0 */ -public class ResourceOverridingShadowingClassLoaderTests { +class ResourceOverridingShadowingClassLoaderTests { private static final String EXISTING_RESOURCE = "org/springframework/instrument/classloading/testResource.xml"; @@ -38,40 +38,40 @@ public class ResourceOverridingShadowingClassLoaderTests { @Test - public void testFindsExistingResourceWithGetResourceAndNoOverrides() { + void testFindsExistingResourceWithGetResourceAndNoOverrides() { assertThat(thisClassLoader.getResource(EXISTING_RESOURCE)).isNotNull(); assertThat(overridingLoader.getResource(EXISTING_RESOURCE)).isNotNull(); } @Test - public void testDoesNotFindExistingResourceWithGetResourceAndNullOverride() { + void testDoesNotFindExistingResourceWithGetResourceAndNullOverride() { assertThat(thisClassLoader.getResource(EXISTING_RESOURCE)).isNotNull(); overridingLoader.override(EXISTING_RESOURCE, null); assertThat(overridingLoader.getResource(EXISTING_RESOURCE)).isNull(); } @Test - public void testFindsExistingResourceWithGetResourceAsStreamAndNoOverrides() { + void testFindsExistingResourceWithGetResourceAsStreamAndNoOverrides() { assertThat(thisClassLoader.getResourceAsStream(EXISTING_RESOURCE)).isNotNull(); assertThat(overridingLoader.getResourceAsStream(EXISTING_RESOURCE)).isNotNull(); } @Test - public void testDoesNotFindExistingResourceWithGetResourceAsStreamAndNullOverride() { + void testDoesNotFindExistingResourceWithGetResourceAsStreamAndNullOverride() { assertThat(thisClassLoader.getResourceAsStream(EXISTING_RESOURCE)).isNotNull(); overridingLoader.override(EXISTING_RESOURCE, null); assertThat(overridingLoader.getResourceAsStream(EXISTING_RESOURCE)).isNull(); } @Test - public void testFindsExistingResourceWithGetResourcesAndNoOverrides() throws IOException { + void testFindsExistingResourceWithGetResourcesAndNoOverrides() throws IOException { assertThat(thisClassLoader.getResources(EXISTING_RESOURCE)).isNotNull(); assertThat(overridingLoader.getResources(EXISTING_RESOURCE)).isNotNull(); assertThat(countElements(overridingLoader.getResources(EXISTING_RESOURCE))).isEqualTo(1); } @Test - public void testDoesNotFindExistingResourceWithGetResourcesAndNullOverride() throws IOException { + void testDoesNotFindExistingResourceWithGetResourcesAndNullOverride() throws IOException { assertThat(thisClassLoader.getResources(EXISTING_RESOURCE)).isNotNull(); overridingLoader.override(EXISTING_RESOURCE, null); assertThat(countElements(overridingLoader.getResources(EXISTING_RESOURCE))).isEqualTo(0); diff --git a/spring-context/src/test/java/org/springframework/jmx/AbstractJmxTests.java b/spring-context/src/test/java/org/springframework/jmx/AbstractJmxTests.java index 7147ccc56dce..037747924f82 100644 --- a/spring-context/src/test/java/org/springframework/jmx/AbstractJmxTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/AbstractJmxTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,12 +32,12 @@ public abstract class AbstractJmxTests extends AbstractMBeanServerTests { @Override - protected final void onSetUp() throws Exception { + protected final void onSetUp() { ctx = loadContext(getApplicationContextPath()); } @Override - protected final void onTearDown() throws Exception { + protected final void onTearDown() { if (ctx != null) { ctx.close(); } diff --git a/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java b/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java index 6078982e1381..a3c4cb43b1ba 100644 --- a/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,12 +73,12 @@ public final void setUp() throws Exception { } @AfterEach - public void tearDown() throws Exception { + protected void tearDown() throws Exception { releaseServer(); onTearDown(); } - private void releaseServer() throws Exception { + private void releaseServer() { try { MBeanServerFactory.releaseMBeanServer(getServer()); } @@ -101,7 +101,7 @@ protected final ConfigurableApplicationContext loadContext(String configLocation protected void onSetUp() throws Exception { } - protected void onTearDown() throws Exception { + protected void onTearDown() { } protected final MBeanServer getServer() { diff --git a/spring-context/src/test/java/org/springframework/jmx/access/MBeanClientInterceptorTests.java b/spring-context/src/test/java/org/springframework/jmx/access/MBeanClientInterceptorTests.java index 8f62afb94633..5577fbd32af9 100644 --- a/spring-context/src/test/java/org/springframework/jmx/access/MBeanClientInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/access/MBeanClientInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -171,7 +171,7 @@ void invokeArgs() throws Exception { void invokeUnexposedMethodWithException() throws Exception { assumeTrue(runTests); IJmxTestBean bean = getProxy(); - assertThatExceptionOfType(InvalidInvocationException.class).isThrownBy(() -> bean.dontExposeMe()); + assertThatExceptionOfType(InvalidInvocationException.class).isThrownBy(bean::dontExposeMe); } @Test diff --git a/spring-context/src/test/java/org/springframework/jmx/access/RemoteMBeanClientInterceptorTests.java b/spring-context/src/test/java/org/springframework/jmx/access/RemoteMBeanClientInterceptorTests.java index 336aaa7bff12..e1d0c8e0e5b9 100644 --- a/spring-context/src/test/java/org/springframework/jmx/access/RemoteMBeanClientInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/access/RemoteMBeanClientInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,7 +75,7 @@ protected MBeanServerConnection getServerConnection() throws Exception { @AfterEach @Override - public void tearDown() throws Exception { + protected void tearDown() throws Exception { if (this.connector != null) { this.connector.close(); } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/LazyInitMBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/export/LazyInitMBeanTests.java index 833357852325..b5a6c672e8d7 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/LazyInitMBeanTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/LazyInitMBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,22 +30,19 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class LazyInitMBeanTests { +class LazyInitMBeanTests { @Test - public void invokeOnLazyInitBean() throws Exception { + void invokeOnLazyInitBean() throws Exception { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("org/springframework/jmx/export/lazyInit.xml"); - assertThat(ctx.getBeanFactory().containsSingleton("testBean")).isFalse(); - assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); - try { + try (ctx) { + assertThat(ctx.getBeanFactory().containsSingleton("testBean")).isFalse(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); MBeanServer server = (MBeanServer) ctx.getBean("server"); ObjectName oname = ObjectNameManager.getInstance("bean:name=testBean2"); String name = (String) server.getAttribute(oname, "Name"); assertThat(name).as("Invalid name returned").isEqualTo("foo"); } - finally { - ctx.close(); - } } } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java index 2fb7bed7f99f..f35d16217602 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import javax.management.Attribute; import javax.management.InstanceNotFoundException; -import javax.management.JMException; import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; import javax.management.Notification; @@ -48,6 +47,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.testfixture.jmx.export.Person; import org.springframework.jmx.AbstractMBeanServerTests; import org.springframework.jmx.IJmxTestBean; import org.springframework.jmx.JmxTestBean; @@ -76,7 +76,7 @@ * @author Sam Brannen * @author Stephane Nicoll */ -public class MBeanExporterTests extends AbstractMBeanServerTests { +class MBeanExporterTests extends AbstractMBeanServerTests { private static final String OBJECT_NAME = "spring:test=jmxMBeanAdaptor"; @@ -84,7 +84,7 @@ public class MBeanExporterTests extends AbstractMBeanServerTests { @Test - void registerNullNotificationListenerType() throws Exception { + void registerNullNotificationListenerType() { Map listeners = new HashMap<>(); // put null in as a value... listeners.put("*", null); @@ -95,7 +95,7 @@ void registerNullNotificationListenerType() throws Exception { } @Test - void registerNotificationListenerForNonExistentMBean() throws Exception { + void registerNotificationListenerForNonExistentMBean() { NotificationListener dummyListener = (notification, handback) -> { throw new UnsupportedOperationException(); }; @@ -191,7 +191,7 @@ void autodetectLazyMBeans() throws Exception { } @Test - void autodetectNoMBeans() throws Exception { + void autodetectNoMBeans() { try (ConfigurableApplicationContext ctx = load("autodetectNoMBeans.xml")) { ctx.getBean("exporter"); } @@ -360,6 +360,7 @@ void bonaFideMBeanIsNotExportedWhenAutodetectIsTotallyTurnedOff() { } @Test + @SuppressWarnings("deprecation") void onlyBonaFideMBeanIsExportedWhenAutodetectIsMBeanOnly() throws Exception { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); @@ -380,6 +381,7 @@ void onlyBonaFideMBeanIsExportedWhenAutodetectIsMBeanOnly() throws Exception { } @Test + @SuppressWarnings("deprecation") void bonaFideMBeanAndRegularBeanExporterWithAutodetectAll() throws Exception { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); @@ -403,6 +405,7 @@ void bonaFideMBeanAndRegularBeanExporterWithAutodetectAll() throws Exception { } @Test + @SuppressWarnings("deprecation") void bonaFideMBeanIsNotExportedWithAutodetectAssembler() throws Exception { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); @@ -425,6 +428,7 @@ void bonaFideMBeanIsNotExportedWithAutodetectAssembler() throws Exception { * Want to ensure that said MBean is not exported twice. */ @Test + @SuppressWarnings("deprecation") void bonaFideMBeanExplicitlyExportedAndAutodetectionIsOn() throws Exception { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); @@ -442,6 +446,7 @@ void bonaFideMBeanExplicitlyExportedAndAutodetectionIsOn() throws Exception { } @Test + @SuppressWarnings("deprecation") void setAutodetectModeToOutOfRangeNegativeValue() { assertThatIllegalArgumentException() .isThrownBy(() -> exporter.setAutodetectMode(-1)) @@ -450,6 +455,7 @@ void setAutodetectModeToOutOfRangeNegativeValue() { } @Test + @SuppressWarnings("deprecation") void setAutodetectModeToOutOfRangePositiveValue() { assertThatIllegalArgumentException() .isThrownBy(() -> exporter.setAutodetectMode(5)) @@ -462,6 +468,7 @@ void setAutodetectModeToOutOfRangePositiveValue() { * configured for all autodetect constants defined in {@link MBeanExporter}. */ @Test + @SuppressWarnings("deprecation") void setAutodetectModeToAllSupportedValues() { streamAutodetectConstants() .map(MBeanExporterTests::getFieldValue) @@ -469,12 +476,14 @@ void setAutodetectModeToAllSupportedValues() { } @Test + @SuppressWarnings("deprecation") void setAutodetectModeToSupportedValue() { exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ASSEMBLER); assertThat(exporter.autodetectMode).isEqualTo(MBeanExporter.AUTODETECT_ASSEMBLER); } @Test + @SuppressWarnings("deprecation") void setAutodetectModeNameToNull() { assertThatIllegalArgumentException() .isThrownBy(() -> exporter.setAutodetectModeName(null)) @@ -483,6 +492,7 @@ void setAutodetectModeNameToNull() { } @Test + @SuppressWarnings("deprecation") void setAutodetectModeNameToAnEmptyString() { assertThatIllegalArgumentException() .isThrownBy(() -> exporter.setAutodetectModeName("")) @@ -491,6 +501,7 @@ void setAutodetectModeNameToAnEmptyString() { } @Test + @SuppressWarnings("deprecation") void setAutodetectModeNameToWhitespace() { assertThatIllegalArgumentException() .isThrownBy(() -> exporter.setAutodetectModeName(" \t")) @@ -499,6 +510,7 @@ void setAutodetectModeNameToWhitespace() { } @Test + @SuppressWarnings("deprecation") void setAutodetectModeNameToBogusValue() { assertThatIllegalArgumentException() .isThrownBy(() -> exporter.setAutodetectModeName("Bogus")) @@ -511,6 +523,7 @@ void setAutodetectModeNameToBogusValue() { * configured for all autodetect constants defined in {@link MBeanExporter}. */ @Test + @SuppressWarnings("deprecation") void setAutodetectModeNameToAllSupportedValues() { streamAutodetectConstants() .map(Field::getName) @@ -518,6 +531,7 @@ void setAutodetectModeNameToAllSupportedValues() { } @Test + @SuppressWarnings("deprecation") void setAutodetectModeNameToSupportedValue() { exporter.setAutodetectModeName("AUTODETECT_ASSEMBLER"); assertThat(exporter.autodetectMode).isEqualTo(MBeanExporter.AUTODETECT_ASSEMBLER); @@ -683,10 +697,8 @@ private static Map getBeanMap() { private static void assertListener(MockMBeanExporterListener listener) throws MalformedObjectNameException { ObjectName desired = ObjectNameManager.getInstance(OBJECT_NAME); - assertThat(listener.getRegistered()).as("Incorrect number of registrations").hasSize(1); - assertThat(listener.getUnregistered()).as("Incorrect number of unregistrations").hasSize(1); - assertThat(listener.getRegistered().get(0)).as("Incorrect ObjectName in register").isEqualTo(desired); - assertThat(listener.getUnregistered().get(0)).as("Incorrect ObjectName in unregister").isEqualTo(desired); + assertThat(listener.getRegistered()).singleElement().isEqualTo(desired); + assertThat(listener.getUnregistered()).singleElement().isEqualTo(desired); } @@ -695,7 +707,7 @@ private static class InvokeDetectAssembler implements MBeanInfoAssembler { private boolean invoked = false; @Override - public ModelMBeanInfo getMBeanInfo(Object managedResource, String beanKey) throws JMException { + public ModelMBeanInfo getMBeanInfo(Object managedResource, String beanKey) { invoked = true; return null; } @@ -752,33 +764,12 @@ public void setObjectName(ObjectName objectName) { } @Override - public ObjectName getObjectName() throws MalformedObjectNameException { + public ObjectName getObjectName() { return this.objectName; } } - public interface PersonMBean { - - String getName(); - } - - - public static class Person implements PersonMBean { - - private String name; - - @Override - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static final class StubNotificationListener implements NotificationListener { private List notifications = new ArrayList<>(); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/NotificationListenerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/NotificationListenerTests.java index f774744dec8c..22dae40db9e7 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/NotificationListenerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/NotificationListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import javax.management.Attribute; import javax.management.AttributeChangeNotification; -import javax.management.MalformedObjectNameException; import javax.management.Notification; import javax.management.NotificationListener; import javax.management.ObjectName; @@ -43,11 +42,11 @@ * @author Mark Fisher * @author Sam Brannen */ -public class NotificationListenerTests extends AbstractMBeanServerTests { +class NotificationListenerTests extends AbstractMBeanServerTests { @SuppressWarnings({"rawtypes", "unchecked"}) @Test - public void testRegisterNotificationListenerForMBean() throws Exception { + void testRegisterNotificationListenerForMBean() throws Exception { ObjectName objectName = ObjectName.getInstance("spring:name=Test"); JmxTestBean bean = new JmxTestBean(); @@ -73,7 +72,7 @@ public void testRegisterNotificationListenerForMBean() throws Exception { @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testRegisterNotificationListenerWithWildcard() throws Exception { + void testRegisterNotificationListenerWithWildcard() throws Exception { ObjectName objectName = ObjectName.getInstance("spring:name=Test"); JmxTestBean bean = new JmxTestBean(); @@ -98,7 +97,7 @@ public void testRegisterNotificationListenerWithWildcard() throws Exception { } @Test - public void testRegisterNotificationListenerWithHandback() throws Exception { + void testRegisterNotificationListenerWithHandback() throws Exception { String objectName = "spring:name=Test"; JmxTestBean bean = new JmxTestBean(); @@ -129,7 +128,7 @@ public void testRegisterNotificationListenerWithHandback() throws Exception { } @Test - public void testRegisterNotificationListenerForAllMBeans() throws Exception { + void testRegisterNotificationListenerForAllMBeans() throws Exception { ObjectName objectName = ObjectName.getInstance("spring:name=Test"); JmxTestBean bean = new JmxTestBean(); @@ -156,7 +155,7 @@ public void testRegisterNotificationListenerForAllMBeans() throws Exception { @SuppressWarnings("serial") @Test - public void testRegisterNotificationListenerWithFilter() throws Exception { + void testRegisterNotificationListenerWithFilter() throws Exception { ObjectName objectName = ObjectName.getInstance("spring:name=Test"); JmxTestBean bean = new JmxTestBean(); @@ -194,14 +193,14 @@ public void testRegisterNotificationListenerWithFilter() throws Exception { } @Test - public void testCreationWithNoNotificationListenerSet() { + void testCreationWithNoNotificationListenerSet() { assertThatIllegalArgumentException().as("no NotificationListener supplied").isThrownBy( new NotificationListenerBean()::afterPropertiesSet); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testRegisterNotificationListenerWithBeanNameAndBeanNameInBeansMap() throws Exception { + void testRegisterNotificationListenerWithBeanNameAndBeanNameInBeansMap() throws Exception { String beanName = "testBean"; ObjectName objectName = ObjectName.getInstance("spring:name=Test"); @@ -232,7 +231,7 @@ public void testRegisterNotificationListenerWithBeanNameAndBeanNameInBeansMap() @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testRegisterNotificationListenerWithBeanNameAndBeanInstanceInBeansMap() throws Exception { + void testRegisterNotificationListenerWithBeanNameAndBeanInstanceInBeansMap() throws Exception { String beanName = "testBean"; ObjectName objectName = ObjectName.getInstance("spring:name=Test"); @@ -263,7 +262,7 @@ public void testRegisterNotificationListenerWithBeanNameAndBeanInstanceInBeansMa @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testRegisterNotificationListenerWithBeanNameBeforeObjectNameMappedToSameBeanInstance() throws Exception { + void testRegisterNotificationListenerWithBeanNameBeforeObjectNameMappedToSameBeanInstance() throws Exception { String beanName = "testBean"; ObjectName objectName = ObjectName.getInstance("spring:name=Test"); @@ -295,7 +294,7 @@ public void testRegisterNotificationListenerWithBeanNameBeforeObjectNameMappedTo @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testRegisterNotificationListenerWithObjectNameBeforeBeanNameMappedToSameBeanInstance() throws Exception { + void testRegisterNotificationListenerWithObjectNameBeforeBeanNameMappedToSameBeanInstance() throws Exception { String beanName = "testBean"; ObjectName objectName = ObjectName.getInstance("spring:name=Test"); @@ -327,7 +326,7 @@ public void testRegisterNotificationListenerWithObjectNameBeforeBeanNameMappedTo @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testRegisterNotificationListenerWithTwoBeanNamesMappedToDifferentBeanInstances() throws Exception { + void testRegisterNotificationListenerWithTwoBeanNamesMappedToDifferentBeanInstances() throws Exception { String beanName1 = "testBean1"; String beanName2 = "testBean2"; @@ -370,7 +369,7 @@ public void testRegisterNotificationListenerWithTwoBeanNamesMappedToDifferentBea } @Test - public void testNotificationListenerRegistrar() throws Exception { + void testNotificationListenerRegistrar() throws Exception { ObjectName objectName = ObjectName.getInstance("spring:name=Test"); JmxTestBean bean = new JmxTestBean(); @@ -403,7 +402,7 @@ public void testNotificationListenerRegistrar() throws Exception { } @Test - public void testNotificationListenerRegistrarWithMultipleNames() throws Exception { + void testNotificationListenerRegistrarWithMultipleNames() throws Exception { ObjectName objectName = ObjectName.getInstance("spring:name=Test"); ObjectName objectName2 = ObjectName.getInstance("spring:name=Test2"); JmxTestBean bean = new JmxTestBean(); @@ -468,7 +467,7 @@ public void handleNotification(Notification notification, Object handback) { public int getCount(String attribute) { Integer count = (Integer) this.attributeCounts.get(attribute); - return (count == null) ? 0 : count; + return (count == null ? 0 : count); } public Object getLastHandback(String attributeName) { @@ -488,7 +487,7 @@ public void setObjectName(ObjectName objectName) { } @Override - public ObjectName getObjectName() throws MalformedObjectNameException { + public ObjectName getObjectName() { return this.objectName; } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/NotificationPublisherTests.java b/spring-context/src/test/java/org/springframework/jmx/export/NotificationPublisherTests.java index 786c608c2a5b..17e50b0644e7 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/NotificationPublisherTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/NotificationPublisherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,15 @@ import javax.management.Attribute; import javax.management.AttributeList; -import javax.management.AttributeNotFoundException; import javax.management.DynamicMBean; -import javax.management.InvalidAttributeValueException; import javax.management.MBeanAttributeInfo; import javax.management.MBeanConstructorInfo; -import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.MBeanNotificationInfo; import javax.management.MBeanOperationInfo; import javax.management.Notification; import javax.management.NotificationBroadcasterSupport; import javax.management.NotificationListener; -import javax.management.ReflectionException; import org.junit.jupiter.api.Test; @@ -48,12 +44,12 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class NotificationPublisherTests extends AbstractMBeanServerTests { +class NotificationPublisherTests extends AbstractMBeanServerTests { private CountingNotificationListener listener = new CountingNotificationListener(); @Test - public void testSimpleBean() throws Exception { + void testSimpleBean() throws Exception { // start the MBeanExporter ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherTests.xml"); this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=Publisher"), listener, null, @@ -66,7 +62,7 @@ public void testSimpleBean() throws Exception { } @Test - public void testSimpleBeanRegisteredManually() throws Exception { + void testSimpleBeanRegisteredManually() throws Exception { // start the MBeanExporter ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherTests.xml"); MBeanExporter exporter = (MBeanExporter) ctx.getBean("exporter"); @@ -81,7 +77,7 @@ public void testSimpleBeanRegisteredManually() throws Exception { } @Test - public void testMBean() throws Exception { + void testMBean() throws Exception { // start the MBeanExporter ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherTests.xml"); this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=PublisherMBean"), listener, @@ -94,7 +90,7 @@ public void testMBean() throws Exception { /* @Test - public void testStandardMBean() throws Exception { + void testStandardMBean() throws Exception { // start the MBeanExporter ApplicationContext ctx = new ClassPathXmlApplicationContext("org/springframework/jmx/export/notificationPublisherTests.xml"); this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=PublisherStandardMBean"), listener, null, null); @@ -106,7 +102,7 @@ public void testStandardMBean() throws Exception { */ @Test - public void testLazyInit() throws Exception { + void testLazyInit() throws Exception { // start the MBeanExporter ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherLazyTests.xml"); assertThat(ctx.getBeanFactory().containsSingleton("publisher")).as("Should not have instantiated the bean yet").isFalse(); @@ -170,14 +166,12 @@ public String getName() { public static class MyNotificationPublisherMBean extends NotificationBroadcasterSupport implements DynamicMBean { @Override - public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, - ReflectionException { + public Object getAttribute(String attribute) { return null; } @Override - public void setAttribute(Attribute attribute) throws AttributeNotFoundException, - InvalidAttributeValueException, MBeanException, ReflectionException { + public void setAttribute(Attribute attribute) { } @Override @@ -191,8 +185,7 @@ public AttributeList setAttributes(AttributeList attributes) { } @Override - public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, - ReflectionException { + public Object invoke(String actionName, Object[] params, String[] signature) { return null; } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java index e58d826c4fac..9ca391f16e8f 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,13 +32,13 @@ * @author Rob Harrop * @author Chris Beams */ -public class AnnotationMetadataAssemblerTests extends AbstractMetadataAssemblerTests { +class AnnotationMetadataAssemblerTests extends AbstractMetadataAssemblerTests { private static final String OBJECT_NAME = "bean:name=testBean4"; @Test - public void testAttributeFromInterface() throws Exception { + void testAttributeFromInterface() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = inf.getAttribute("Colour"); assertThat(attr.isWritable()).as("The name attribute should be writable").isTrue(); @@ -46,21 +46,21 @@ public void testAttributeFromInterface() throws Exception { } @Test - public void testOperationFromInterface() throws Exception { + void testOperationFromInterface() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanOperationInfo op = inf.getOperation("fromInterface"); assertThat(op).isNotNull(); } @Test - public void testOperationOnGetter() throws Exception { + void testOperationOnGetter() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanOperationInfo op = inf.getOperation("getExpensiveToCalculate"); assertThat(op).isNotNull(); } @Test - public void testRegistrationOnInterface() throws Exception { + void testRegistrationOnInterface() throws Exception { Object bean = getContext().getBean("testInterfaceBean"); ModelMBeanInfo inf = getAssembler().getMBeanInfo(bean, "bean:name=interfaceTestBean"); assertThat(inf).isNotNull(); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBeanFactory.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBeanFactory.java index 1634b6b75c3c..814598b4d5eb 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBeanFactory.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ public AnnotationTestBeanFactory() { } @Override - public FactoryCreatedAnnotationTestBean getObject() throws Exception { + public FactoryCreatedAnnotationTestBean getObject() { return this.instance; } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/EnableMBeanExportConfigurationTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/EnableMBeanExportConfigurationTests.java index 553a4cb14cdc..1731ec52a2cf 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/annotation/EnableMBeanExportConfigurationTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/EnableMBeanExportConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ import org.springframework.context.annotation.MBeanExportConfiguration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.jmx.export.MBeanExporterTests; +import org.springframework.context.testfixture.jmx.export.Person; import org.springframework.jmx.export.TestDynamicMBean; import org.springframework.jmx.export.metadata.InvalidMetadataException; import org.springframework.jmx.support.MBeanServerFactoryBean; @@ -49,13 +49,13 @@ * @author Stephane Nicoll * @see AnnotationLazyInitMBeanTests */ -public class EnableMBeanExportConfigurationTests { +class EnableMBeanExportConfigurationTests { private AnnotationConfigApplicationContext ctx; @AfterEach - public void closeContext() { + void closeContext() { if (this.ctx != null) { this.ctx.close(); } @@ -63,7 +63,7 @@ public void closeContext() { @Test - public void testLazyNaming() throws Exception { + void testLazyNaming() throws Exception { load(LazyNamingConfiguration.class); validateAnnotationTestBean(); } @@ -73,7 +73,7 @@ private void load(Class... config) { } @Test - public void testOnlyTargetClassIsExposed() throws Exception { + void testOnlyTargetClassIsExposed() throws Exception { load(ProxyConfiguration.class); validateAnnotationTestBean(); } @@ -97,13 +97,13 @@ public void testPackagePrivateImplementationCantBeExposed() { } @Test - public void testPackagePrivateClassExtensionCanBeExposed() throws Exception { + void testPackagePrivateClassExtensionCanBeExposed() throws Exception { load(PackagePrivateExtensionConfiguration.class); validateAnnotationTestBean(); } @Test - public void testPlaceholderBased() throws Exception { + void testPlaceholderBased() throws Exception { MockEnvironment env = new MockEnvironment(); env.setProperty("serverName", "server"); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @@ -115,7 +115,7 @@ public void testPlaceholderBased() throws Exception { } @Test - public void testLazyAssembling() throws Exception { + void testLazyAssembling() throws Exception { System.setProperty("domain", "bean"); load(LazyAssemblingConfiguration.class); try { @@ -132,7 +132,7 @@ public void testLazyAssembling() throws Exception { } @Test - public void testComponentScan() throws Exception { + void testComponentScan() throws Exception { load(ComponentScanConfiguration.class); MBeanServer server = (MBeanServer) this.ctx.getBean("server"); validateMBeanAttribute(server, "bean:name=testBean4", null); @@ -242,8 +242,8 @@ public TestDynamicMBean dynamic() { @Bean(name="spring:mbean=another") @Lazy - public MBeanExporterTests.Person person() { - MBeanExporterTests.Person person = new MBeanExporterTests.Person(); + public Person person() { + Person person = new Person(); person.setName("Juergen Hoeller"); return person; } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/JmxUtilsAnnotationTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/JmxUtilsAnnotationTests.java index b96118a8820b..bfe2eaacb38b 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/annotation/JmxUtilsAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/JmxUtilsAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,15 +27,15 @@ /** * @author Juergen Hoeller */ -public class JmxUtilsAnnotationTests { +class JmxUtilsAnnotationTests { @Test - public void notMXBean() throws Exception { + void notMXBean() { assertThat(JmxUtils.isMBean(FooNotX.class)).as("MXBean annotation not detected correctly").isFalse(); } @Test - public void annotatedMXBean() throws Exception { + void annotatedMXBean() { assertThat(JmxUtils.isMBean(FooX.class)).as("MXBean annotation not detected correctly").isTrue(); } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java index b685aa9caf78..fe29582805f8 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,14 +49,14 @@ public abstract class AbstractJmxAssemblerTests extends AbstractJmxTests { protected abstract String getObjectName(); @Test - public void testMBeanRegistration() throws Exception { + void testMBeanRegistration() throws Exception { // beans are registered at this point - just grab them from the server ObjectInstance instance = getObjectInstance(); assertThat(instance).as("Bean should not be null").isNotNull(); } @Test - public void testRegisterOperations() throws Exception { + void testRegisterOperations() throws Exception { IJmxTestBean bean = getBean(); assertThat(bean).isNotNull(); MBeanInfo inf = getMBeanInfo(); @@ -64,7 +64,7 @@ public void testRegisterOperations() throws Exception { } @Test - public void testRegisterAttributes() throws Exception { + void testRegisterAttributes() throws Exception { IJmxTestBean bean = getBean(); assertThat(bean).isNotNull(); MBeanInfo inf = getMBeanInfo(); @@ -72,13 +72,13 @@ public void testRegisterAttributes() throws Exception { } @Test - public void testGetMBeanInfo() throws Exception { + void testGetMBeanInfo() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); assertThat(info).as("MBeanInfo should not be null").isNotNull(); } @Test - public void testGetMBeanAttributeInfo() throws Exception { + void testGetMBeanAttributeInfo() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); MBeanAttributeInfo[] inf = info.getAttributes(); assertThat(inf).as("Invalid number of Attributes returned").hasSize(getExpectedAttributeCount()); @@ -90,7 +90,7 @@ public void testGetMBeanAttributeInfo() throws Exception { } @Test - public void testGetMBeanOperationInfo() throws Exception { + void testGetMBeanOperationInfo() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); MBeanOperationInfo[] inf = info.getOperations(); assertThat(inf).as("Invalid number of Operations returned").hasSize(getExpectedOperationCount()); @@ -102,14 +102,14 @@ public void testGetMBeanOperationInfo() throws Exception { } @Test - public void testDescriptionNotNull() throws Exception { + void testDescriptionNotNull() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); assertThat(info.getDescription()).as("The MBean description should not be null").isNotNull(); } @Test - public void testSetAttribute() throws Exception { + void testSetAttribute() throws Exception { ObjectName objectName = ObjectNameManager.getInstance(getObjectName()); getServer().setAttribute(objectName, new Attribute(NAME_ATTRIBUTE, "Rob Harrop")); IJmxTestBean bean = (IJmxTestBean) getContext().getBean("testBean"); @@ -117,7 +117,7 @@ public void testSetAttribute() throws Exception { } @Test - public void testGetAttribute() throws Exception { + void testGetAttribute() throws Exception { ObjectName objectName = ObjectNameManager.getInstance(getObjectName()); getBean().setName("John Smith"); Object val = getServer().getAttribute(objectName, NAME_ATTRIBUTE); @@ -125,7 +125,7 @@ public void testGetAttribute() throws Exception { } @Test - public void testOperationInvocation() throws Exception{ + void testOperationInvocation() throws Exception{ ObjectName objectName = ObjectNameManager.getInstance(getObjectName()); Object result = getServer().invoke(objectName, "add", new Object[] {20, 30}, new String[] {"int", "int"}); @@ -133,7 +133,7 @@ public void testOperationInvocation() throws Exception{ } @Test - public void testAttributeInfoHasDescriptors() throws Exception { + void testAttributeInfoHasDescriptors() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(NAME_ATTRIBUTE); @@ -145,7 +145,7 @@ public void testAttributeInfoHasDescriptors() throws Exception { } @Test - public void testAttributeHasCorrespondingOperations() throws Exception { + void testAttributeHasCorrespondingOperations() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanOperationInfo get = info.getOperation("getName"); @@ -160,7 +160,7 @@ public void testAttributeHasCorrespondingOperations() throws Exception { } @Test - public void testNotificationMetadata() throws Exception { + void testNotificationMetadata() throws Exception { ModelMBeanInfo info = (ModelMBeanInfo) getMBeanInfo(); MBeanNotificationInfo[] notifications = info.getNotifications(); assertThat(notifications).as("Incorrect number of notifications").hasSize(1); @@ -175,8 +175,7 @@ public void testNotificationMetadata() throws Exception { protected ModelMBeanInfo getMBeanInfoFromAssembler() throws Exception { IJmxTestBean bean = getBean(); - ModelMBeanInfo info = getAssembler().getMBeanInfo(bean, getObjectName()); - return info; + return getAssembler().getMBeanInfo(bean, getObjectName()); } protected IJmxTestBean getBean() { @@ -196,6 +195,6 @@ protected ObjectInstance getObjectInstance() throws Exception { protected abstract int getExpectedAttributeCount(); - protected abstract MBeanInfoAssembler getAssembler() throws Exception; + protected abstract MBeanInfoAssembler getAssembler(); } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractMetadataAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractMetadataAssemblerTests.java index 7afe8e29546a..31c9f7fd665b 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractMetadataAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractMetadataAssemblerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,20 +49,20 @@ public abstract class AbstractMetadataAssemblerTests extends AbstractJmxAssemble protected static final String CACHE_ENTRIES_METRIC = "CacheEntries"; @Test - public void testDescription() throws Exception { + void testDescription() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); assertThat(info.getDescription()).as("The descriptions are not the same").isEqualTo("My Managed Bean"); } @Test - public void testAttributeDescriptionOnSetter() throws Exception { + void testAttributeDescriptionOnSetter() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = inf.getAttribute(AGE_ATTRIBUTE); assertThat(attr.getDescription()).as("The description for the age attribute is incorrect").isEqualTo("The Age Attribute"); } @Test - public void testAttributeDescriptionOnGetter() throws Exception { + void testAttributeDescriptionOnGetter() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = inf.getAttribute(NAME_ATTRIBUTE); assertThat(attr.getDescription()).as("The description for the name attribute is incorrect").isEqualTo("The Name Attribute"); @@ -72,14 +72,14 @@ public void testAttributeDescriptionOnGetter() throws Exception { * Tests the situation where the attribute is only defined on the getter. */ @Test - public void testReadOnlyAttribute() throws Exception { + void testReadOnlyAttribute() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = inf.getAttribute(AGE_ATTRIBUTE); assertThat(attr.isWritable()).as("The age attribute should not be writable").isFalse(); } @Test - public void testReadWriteAttribute() throws Exception { + void testReadWriteAttribute() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = inf.getAttribute(NAME_ATTRIBUTE); assertThat(attr.isWritable()).as("The name attribute should be writable").isTrue(); @@ -90,7 +90,7 @@ public void testReadWriteAttribute() throws Exception { * Tests the situation where the property only has a getter. */ @Test - public void testWithOnlySetter() throws Exception { + void testWithOnlySetter() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = inf.getAttribute("NickName"); assertThat(attr).as("Attribute should not be null").isNotNull(); @@ -100,14 +100,14 @@ public void testWithOnlySetter() throws Exception { * Tests the situation where the property only has a setter. */ @Test - public void testWithOnlyGetter() throws Exception { + void testWithOnlyGetter() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute("Superman"); assertThat(attr).as("Attribute should not be null").isNotNull(); } @Test - public void testManagedResourceDescriptor() throws Exception { + void testManagedResourceDescriptor() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); Descriptor desc = info.getMBeanDescriptor(); @@ -121,7 +121,7 @@ public void testManagedResourceDescriptor() throws Exception { } @Test - public void testAttributeDescriptor() throws Exception { + void testAttributeDescriptor() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); Descriptor desc = info.getAttribute(NAME_ATTRIBUTE).getDescriptor(); @@ -132,7 +132,7 @@ public void testAttributeDescriptor() throws Exception { } @Test - public void testOperationDescriptor() throws Exception { + void testOperationDescriptor() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); Descriptor desc = info.getOperation("myOperation").getDescriptor(); @@ -141,7 +141,7 @@ public void testOperationDescriptor() throws Exception { } @Test - public void testOperationParameterMetadata() throws Exception { + void testOperationParameterMetadata() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanOperationInfo oper = info.getOperation("add"); MBeanParameterInfo[] params = oper.getSignature(); @@ -155,7 +155,7 @@ public void testOperationParameterMetadata() throws Exception { } @Test - public void testWithCglibProxy() throws Exception { + void testWithCglibProxy() throws Exception { IJmxTestBean tb = createJmxTestBean(); ProxyFactory pf = new ProxyFactory(); pf.setTarget(tb); @@ -183,7 +183,7 @@ public void testWithCglibProxy() throws Exception { } @Test - public void testMetricDescription() throws Exception { + void testMetricDescription() throws Exception { ModelMBeanInfo inf = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo metric = inf.getAttribute(QUEUE_SIZE_METRIC); ModelMBeanOperationInfo operation = inf.getOperation("getQueueSize"); @@ -192,7 +192,7 @@ public void testMetricDescription() throws Exception { } @Test - public void testMetricDescriptor() throws Exception { + void testMetricDescriptor() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); Descriptor desc = info.getAttribute(QUEUE_SIZE_METRIC).getDescriptor(); assertThat(desc.getFieldValue("currencyTimeLimit")).as("Currency Time Limit should be 20").isEqualTo("20"); @@ -205,7 +205,7 @@ public void testMetricDescriptor() throws Exception { } @Test - public void testMetricDescriptorDefaults() throws Exception { + void testMetricDescriptorDefaults() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); Descriptor desc = info.getAttribute(CACHE_ENTRIES_METRIC).getDescriptor(); assertThat(desc.getFieldValue("currencyTimeLimit")).as("Currency Time Limit should not be populated").isNull(); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerCustomTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerCustomTests.java index a3a2a8082526..839de1f54343 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerCustomTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerCustomTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ * @author Rob Harrop * @author Chris Beams */ -public class InterfaceBasedMBeanInfoAssemblerCustomTests extends AbstractJmxAssemblerTests { +class InterfaceBasedMBeanInfoAssemblerCustomTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean5"; @@ -55,7 +55,7 @@ protected MBeanInfoAssembler getAssembler() { } @Test - public void testGetAgeIsReadOnly() throws Exception { + void testGetAgeIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerMappedTests.java index 15cc8a35e555..b7b2623eec7c 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerMappedTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerMappedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,12 @@ * @author Rob Harrop * @author Chris Beams */ -public class InterfaceBasedMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { +class InterfaceBasedMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean4"; @Test - public void testGetAgeIsReadOnly() throws Exception { + void testGetAgeIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); @@ -45,19 +45,19 @@ public void testGetAgeIsReadOnly() throws Exception { } @Test - public void testWithUnknownClass() throws Exception { + void testWithUnknownClass() { assertThatIllegalArgumentException().isThrownBy(() -> getWithMapping("com.foo.bar.Unknown")); } @Test - public void testWithNonInterface() throws Exception { + void testWithNonInterface() { assertThatIllegalArgumentException().isThrownBy(() -> getWithMapping("JmxTestBean")); } @Test - public void testWithFallThrough() throws Exception { + void testWithFallThrough() throws Exception { InterfaceBasedMBeanInfoAssembler assembler = getWithMapping("foobar", "org.springframework.jmx.export.assembler.ICustomJmxBean"); assembler.setManagedInterfaces(new Class[] {IAdditionalTestMethods.class}); @@ -69,7 +69,7 @@ public void testWithFallThrough() throws Exception { } @Test - public void testNickNameIsExposed() throws Exception { + void testNickNameIsExposed() throws Exception { ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); MBeanAttributeInfo attr = inf.getAttribute("NickName"); @@ -92,7 +92,7 @@ protected int getExpectedAttributeCount() { } @Override - protected MBeanInfoAssembler getAssembler() throws Exception { + protected MBeanInfoAssembler getAssembler() { return getWithMapping( "org.springframework.jmx.export.assembler.IAdditionalTestMethods, " + "org.springframework.jmx.export.assembler.ICustomJmxBean"); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerTests.java index 699b56312cc0..69225145ce45 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ /** * @author Rob Harrop */ -public class InterfaceBasedMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { +class InterfaceBasedMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { @Override protected String getObjectName() { diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerComboTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerComboTests.java index 42214760d798..08a05ff4519b 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerComboTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerComboTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,12 @@ * @author Rob Harrop * @author Chris Beams */ -public class MethodExclusionMBeanInfoAssemblerComboTests extends AbstractJmxAssemblerTests { +class MethodExclusionMBeanInfoAssemblerComboTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean4"; @Test - public void testGetAgeIsReadOnly() throws Exception { + void testGetAgeIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); assertThat(attr.isReadable()).as("Age is not readable").isTrue(); @@ -44,7 +44,7 @@ public void testGetAgeIsReadOnly() throws Exception { } @Test - public void testNickNameIsExposed() throws Exception { + void testNickNameIsExposed() throws Exception { ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); MBeanAttributeInfo attr = inf.getAttribute("NickName"); assertThat(attr).as("Nick Name should not be null").isNotNull(); @@ -73,7 +73,7 @@ protected String getApplicationContextPath() { } @Override - protected MBeanInfoAssembler getAssembler() throws Exception { + protected MBeanInfoAssembler getAssembler() { MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); Properties props = new Properties(); props.setProperty(OBJECT_NAME, "setAge,isSuperman,setSuperman,dontExposeMe"); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerMappedTests.java index c5a35ccc835b..080b2b19bfdf 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerMappedTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerMappedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,12 +30,12 @@ * @author Rob Harrop * @author Chris Beams */ -public class MethodExclusionMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { +class MethodExclusionMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean4"; @Test - public void testGetAgeIsReadOnly() throws Exception { + void testGetAgeIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); assertThat(attr.isReadable()).as("Age is not readable").isTrue(); @@ -43,7 +43,7 @@ public void testGetAgeIsReadOnly() throws Exception { } @Test - public void testNickNameIsExposed() throws Exception { + void testNickNameIsExposed() throws Exception { ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); MBeanAttributeInfo attr = inf.getAttribute("NickName"); assertThat(attr).as("Nick Name should not be null").isNotNull(); @@ -72,7 +72,7 @@ protected String getApplicationContextPath() { } @Override - protected MBeanInfoAssembler getAssembler() throws Exception { + protected MBeanInfoAssembler getAssembler() { MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); Properties props = new Properties(); props.setProperty(OBJECT_NAME, "setAge,isSuperman,setSuperman,dontExposeMe"); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerNotMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerNotMappedTests.java index 514a75dcd220..50943d36b1bc 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerNotMappedTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerNotMappedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,12 @@ * @author Rob Harrop * @author Chris Beams */ -public class MethodExclusionMBeanInfoAssemblerNotMappedTests extends AbstractJmxAssemblerTests { +class MethodExclusionMBeanInfoAssemblerNotMappedTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean4"; @Test - public void testGetAgeIsReadOnly() throws Exception { + void testGetAgeIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); assertThat(attr.isReadable()).as("Age is not readable").isTrue(); @@ -44,7 +44,7 @@ public void testGetAgeIsReadOnly() throws Exception { } @Test - public void testNickNameIsExposed() throws Exception { + void testNickNameIsExposed() throws Exception { ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); MBeanAttributeInfo attr = inf.getAttribute("NickName"); assertThat(attr).as("Nick Name should not be null").isNotNull(); @@ -73,7 +73,7 @@ protected String getApplicationContextPath() { } @Override - protected MBeanInfoAssembler getAssembler() throws Exception { + protected MBeanInfoAssembler getAssembler() { MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); Properties props = new Properties(); props.setProperty("bean:name=testBean5", "setAge,isSuperman,setSuperman,dontExposeMe"); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerTests.java index ce6fa1bd516f..83f5b1cc25ee 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ * @author Rick Evans * @author Chris Beams */ -public class MethodExclusionMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { +class MethodExclusionMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { private static final String OBJECT_NAME = "bean:name=testBean5"; @@ -66,7 +66,7 @@ protected MBeanInfoAssembler getAssembler() { } @Test - public void testSupermanIsReadOnly() throws Exception { + void testSupermanIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute("Superman"); @@ -78,7 +78,7 @@ public void testSupermanIsReadOnly() throws Exception { * https://opensource.atlassian.com/projects/spring/browse/SPR-2754 */ @Test - public void testIsNotIgnoredDoesntIgnoreUnspecifiedBeanMethods() throws Exception { + void testIsNotIgnoredDoesntIgnoreUnspecifiedBeanMethods() throws Exception { final String beanKey = "myTestBean"; MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); Properties ignored = new Properties(); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerMappedTests.java index 875d932d09ee..02a1bb59d079 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerMappedTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerMappedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,13 @@ * @author Rob Harrop * @author Chris Beams */ -public class MethodNameBasedMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { +class MethodNameBasedMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean4"; @Test - public void testGetAgeIsReadOnly() throws Exception { + void testGetAgeIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); @@ -45,7 +45,7 @@ public void testGetAgeIsReadOnly() throws Exception { } @Test - public void testWithFallThrough() throws Exception { + void testWithFallThrough() throws Exception { MethodNameBasedMBeanInfoAssembler assembler = getWithMapping("foobar", "add,myOperation,getName,setName,getAge"); assembler.setManagedMethods("getNickName", "setNickName"); @@ -57,7 +57,7 @@ public void testWithFallThrough() throws Exception { } @Test - public void testNickNameIsExposed() throws Exception { + void testNickNameIsExposed() throws Exception { ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); MBeanAttributeInfo attr = inf.getAttribute("NickName"); @@ -80,7 +80,7 @@ protected int getExpectedAttributeCount() { } @Override - protected MBeanInfoAssembler getAssembler() throws Exception { + protected MBeanInfoAssembler getAssembler() { return getWithMapping("getNickName,setNickName,add,myOperation,getName,setName,getAge"); } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerTests.java index f287abb69ef1..6d691f861439 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ * @author David Boden * @author Chris Beams */ -public class MethodNameBasedMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { +class MethodNameBasedMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean5"; @@ -57,7 +57,7 @@ protected MBeanInfoAssembler getAssembler() { } @Test - public void testGetAgeIsReadOnly() throws Exception { + void testGetAgeIsReadOnly() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); @@ -66,7 +66,7 @@ public void testGetAgeIsReadOnly() throws Exception { } @Test - public void testSetNameParameterIsNamed() throws Exception { + void testSetNameParameterIsNamed() throws Exception { ModelMBeanInfo info = getMBeanInfoFromAssembler(); MBeanOperationInfo operationSetAge = info.getOperation("setName"); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/ReflectiveAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ReflectiveAssemblerTests.java index 54b3c5467141..a96de524c052 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/ReflectiveAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ReflectiveAssemblerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ /** * @author Rob Harrop */ -public class ReflectiveAssemblerTests extends AbstractJmxAssemblerTests { +class ReflectiveAssemblerTests extends AbstractJmxAssemblerTests { protected static final String OBJECT_NAME = "bean:name=testBean1"; diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/AbstractNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/AbstractNamingStrategyTests.java index 404c7df367b0..66c6e5ad9aee 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/naming/AbstractNamingStrategyTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/AbstractNamingStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ public abstract class AbstractNamingStrategyTests { @Test - public void naming() throws Exception { + void naming() throws Exception { ObjectNamingStrategy strat = getStrategy(); ObjectName objectName = strat.getObjectName(getManagedResource(), getKey()); assertThat(getCorrectObjectName()).isEqualTo(objectName.getCanonicalName()); @@ -36,7 +36,7 @@ public void naming() throws Exception { protected abstract ObjectNamingStrategy getStrategy() throws Exception; - protected abstract Object getManagedResource() throws Exception; + protected abstract Object getManagedResource(); protected abstract String getKey(); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/IdentityNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/IdentityNamingStrategyTests.java index 3aa4baa2053f..ad99a3b4f7a2 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/naming/IdentityNamingStrategyTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/IdentityNamingStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ /** * @author Rob Harrop */ -public class IdentityNamingStrategyTests { +class IdentityNamingStrategyTests { @Test - public void naming() throws MalformedObjectNameException { + void naming() throws MalformedObjectNameException { JmxTestBean bean = new JmxTestBean(); IdentityNamingStrategy strategy = new IdentityNamingStrategy(); ObjectName objectName = strategy.getObjectName(bean, "null"); diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/KeyNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/KeyNamingStrategyTests.java index 53b64367e3d9..c16d05e6a5a8 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/naming/KeyNamingStrategyTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/KeyNamingStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,13 @@ /** * @author Rob Harrop */ -public class KeyNamingStrategyTests extends AbstractNamingStrategyTests { +class KeyNamingStrategyTests extends AbstractNamingStrategyTests { private static final String OBJECT_NAME = "spring:name=test"; @Override - protected ObjectNamingStrategy getStrategy() throws Exception { + protected ObjectNamingStrategy getStrategy() { return new KeyNamingStrategy(); } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/MetadataNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/MetadataNamingStrategyTests.java new file mode 100644 index 000000000000..a29abc4fa3a9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/MetadataNamingStrategyTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jmx.export.naming; + +import java.util.function.Consumer; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + + +/** + * Tests for {@link MetadataNamingStrategy}. + * + * @author Stephane Nicoll + */ +class MetadataNamingStrategyTests { + + private static final TestBean TEST_BEAN = new TestBean(); + + private final MetadataNamingStrategy strategy; + + MetadataNamingStrategyTests() { + this.strategy = new MetadataNamingStrategy(); + this.strategy.setDefaultDomain("com.example"); + this.strategy.setAttributeSource(new AnnotationJmxAttributeSource()); + } + + @Test + void getObjectNameWhenBeanNameIsSimple() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "myBean"); + assertThat(name.getDomain()).isEqualTo("com.example"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "myBean")); + } + + @Test + void getObjectNameWhenBeanNameIsValidObjectName() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "com.another:name=myBean"); + assertThat(name.getDomain()).isEqualTo("com.another"); + assertThat(name.getKeyPropertyList()).containsOnly(entry("name", "myBean")); + } + + @Test + void getObjectNameWhenBeanNameContainsComma() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "myBean,"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"myBean,\"")); + } + + @Test + void getObjectNameWhenBeanNameContainsEquals() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "my=Bean"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"my=Bean\"")); + } + + @Test + void getObjectNameWhenBeanNameContainsColon() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "my:Bean"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"my:Bean\"")); + } + + @Test + void getObjectNameWhenBeanNameContainsQuote() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "\"myBean\""); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"\\\"myBean\\\"\"")); + } + + private Consumer hasDefaultProperties(Object instance, String expectedName) { + return objectName -> assertThat(objectName.getKeyPropertyList()).containsOnly( + entry("type", ClassUtils.getShortName(instance.getClass())), + entry("name", expectedName)); + } + + static class TestBean {} + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesFileNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesFileNamingStrategyTests.java index aaade22dcb4e..39c21311d617 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesFileNamingStrategyTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesFileNamingStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ /** * @author Juergen Hoeller */ -public class PropertiesFileNamingStrategyTests extends PropertiesNamingStrategyTests { +class PropertiesFileNamingStrategyTests extends PropertiesNamingStrategyTests { @Override protected ObjectNamingStrategy getStrategy() throws Exception { diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesNamingStrategyTests.java index d2a2a41ae49f..e1e58562e858 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesNamingStrategyTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesNamingStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class PropertiesNamingStrategyTests extends AbstractNamingStrategyTests { +class PropertiesNamingStrategyTests extends AbstractNamingStrategyTests { private static final String OBJECT_NAME = "bean:name=namingTest"; diff --git a/spring-context/src/test/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisherTests.java b/spring-context/src/test/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisherTests.java index b8d28a9b23df..f1def7aede8e 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisherTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,35 +34,36 @@ * @author Rick Evans * @author Chris Beams */ -public class ModelMBeanNotificationPublisherTests { +class ModelMBeanNotificationPublisherTests { @Test - public void testCtorWithNullMBean() throws Exception { + void testCtorWithNullMBean() { assertThatIllegalArgumentException().isThrownBy(() -> new ModelMBeanNotificationPublisher(null, createObjectName(), this)); } @Test - public void testCtorWithNullObjectName() throws Exception { + void testCtorWithNullObjectName() { assertThatIllegalArgumentException().isThrownBy(() -> new ModelMBeanNotificationPublisher(new SpringModelMBean(), null, this)); } @Test - public void testCtorWithNullManagedResource() throws Exception { + void testCtorWithNullManagedResource() { assertThatIllegalArgumentException().isThrownBy(() -> new ModelMBeanNotificationPublisher(new SpringModelMBean(), createObjectName(), null)); } @Test - public void testSendNullNotification() throws Exception { + void testSendNullNotification() throws Exception { NotificationPublisher publisher = new ModelMBeanNotificationPublisher(new SpringModelMBean(), createObjectName(), this); assertThatIllegalArgumentException().isThrownBy(() -> publisher.sendNotification(null)); } - public void testSendVanillaNotification() throws Exception { + @Test + void testSendVanillaNotification() throws Exception { StubSpringModelMBean mbean = new StubSpringModelMBean(); Notification notification = new Notification("network.alarm.router", mbean, 1872); ObjectName objectName = createObjectName(); @@ -75,7 +76,8 @@ public void testSendVanillaNotification() throws Exception { assertThat(mbean.getActualNotification().getSource()).as("The 'source' property of the Notification is not being set to the ObjectName of the associated MBean.").isSameAs(objectName); } - public void testSendAttributeChangeNotification() throws Exception { + @Test + void testSendAttributeChangeNotification() throws Exception { StubSpringModelMBean mbean = new StubSpringModelMBean(); Notification notification = new AttributeChangeNotification(mbean, 1872, System.currentTimeMillis(), "Shall we break for some tea?", "agree", "java.lang.Boolean", Boolean.FALSE, Boolean.TRUE); ObjectName objectName = createObjectName(); @@ -90,7 +92,8 @@ public void testSendAttributeChangeNotification() throws Exception { assertThat(mbean.getActualNotification().getSource()).as("The 'source' property of the Notification is not being set to the ObjectName of the associated MBean.").isSameAs(objectName); } - public void testSendAttributeChangeNotificationWhereSourceIsNotTheManagedResource() throws Exception { + @Test + void testSendAttributeChangeNotificationWhereSourceIsNotTheManagedResource() throws Exception { StubSpringModelMBean mbean = new StubSpringModelMBean(); Notification notification = new AttributeChangeNotification(this, 1872, System.currentTimeMillis(), "Shall we break for some tea?", "agree", "java.lang.Boolean", Boolean.FALSE, Boolean.TRUE); ObjectName objectName = createObjectName(); diff --git a/spring-context/src/test/java/org/springframework/jmx/support/ConnectorServerFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/support/ConnectorServerFactoryBeanTests.java index 5f349774451f..db4d84a032b0 100644 --- a/spring-context/src/test/java/org/springframework/jmx/support/ConnectorServerFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/support/ConnectorServerFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.jmx.support; import java.io.IOException; -import java.net.MalformedURLException; import javax.management.InstanceNotFoundException; import javax.management.MBeanServer; @@ -47,7 +46,6 @@ class ConnectorServerFactoryBeanTests extends AbstractMBeanServerTests { private static final String OBJECT_NAME = "spring:type=connector,name=test"; - @SuppressWarnings("deprecation") private final String serviceUrl = "service:jmx:jmxmp://localhost:" + TestSocketUtils.findAvailableTcpPort(); @@ -112,7 +110,7 @@ void noRegisterWithMBeanServer() throws Exception { } } - private void checkServerConnection(MBeanServer hostedServer) throws IOException, MalformedURLException { + private void checkServerConnection(MBeanServer hostedServer) throws IOException { // Try to connect using client. JMXServiceURL serviceURL = new JMXServiceURL(this.serviceUrl); JMXConnector connector = JMXConnectorFactory.connect(serviceURL); diff --git a/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java b/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java index c929150af1c9..c81946ee61ae 100644 --- a/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link JmxUtils}. + * Tests for {@link JmxUtils}. * * @author Rob Harrop * @author Juergen Hoeller @@ -58,7 +58,7 @@ void isMBean() { } @Test - void isMBeanWithDynamicMBean() { + void isMBeanWithDynamicMBean() { DynamicMBean mbean = new TestDynamicMBean(); assertThat(JmxUtils.isMBean(mbean.getClass())).as("Dynamic MBean not detected correctly").isTrue(); } @@ -76,24 +76,24 @@ void isMBeanWithStandardMBeanInherited() throws NotCompliantMBeanException { } @Test - void notAnMBean() { + void notAnMBean() { assertThat(JmxUtils.isMBean(Object.class)).as("Object incorrectly identified as an MBean").isFalse(); } @Test - void simpleMBean() { + void simpleMBean() { Foo foo = new Foo(); assertThat(JmxUtils.isMBean(foo.getClass())).as("Simple MBean not detected correctly").isTrue(); } @Test - void simpleMXBean() { + void simpleMXBean() { FooX foo = new FooX(); assertThat(JmxUtils.isMBean(foo.getClass())).as("Simple MXBean not detected correctly").isTrue(); } @Test - void simpleMBeanThroughInheritance() { + void simpleMBeanThroughInheritance() { Bar bar = new Bar(); Abc abc = new Abc(); assertThat(JmxUtils.isMBean(bar.getClass())).as("Simple MBean (through inheritance) not detected correctly").isTrue(); diff --git a/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBeanTests.java index 6391f607f93c..51fc4e44e311 100644 --- a/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ class MBeanServerConnectionFactoryBeanTests extends AbstractMBeanServerTests { @Test - void noServiceUrl() throws Exception { + void noServiceUrl() { MBeanServerConnectionFactoryBean bean = new MBeanServerConnectionFactoryBean(); assertThatIllegalArgumentException() .isThrownBy(bean::afterPropertiesSet) diff --git a/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerFactoryBeanTests.java index a0092708c58e..e35bc130cfbe 100644 --- a/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ class MBeanServerFactoryBeanTests { @BeforeEach @AfterEach - void resetMBeanServers() throws Exception { + void resetMBeanServers() { MBeanTestUtils.resetMBeanServers(); } @@ -120,12 +120,12 @@ void withEmptyAgentIdAndFallbackToPlatformServer() { } @Test - void createMBeanServer() throws Exception { + void createMBeanServer() { assertCreation(true, "The server should be available in the list"); } @Test - void newMBeanServer() throws Exception { + void newMBeanServer() { assertCreation(false, "The server should not be available in the list"); } diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiObjectFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiObjectFactoryBeanTests.java index 4b9c4f7af237..f33f555dfcb2 100644 --- a/spring-context/src/test/java/org/springframework/jndi/JndiObjectFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/jndi/JndiObjectFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,16 +41,16 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class JndiObjectFactoryBeanTests { +class JndiObjectFactoryBeanTests { @Test - public void testNoJndiName() throws NamingException { + void testNoJndiName() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); assertThatIllegalArgumentException().isThrownBy(jof::afterPropertiesSet); } @Test - public void testLookupWithFullNameAndResourceRefTrue() throws Exception { + void testLookupWithFullNameAndResourceRefTrue() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); Object o = new Object(); jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); @@ -61,7 +61,7 @@ public void testLookupWithFullNameAndResourceRefTrue() throws Exception { } @Test - public void testLookupWithFullNameAndResourceRefFalse() throws Exception { + void testLookupWithFullNameAndResourceRefFalse() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); Object o = new Object(); jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); @@ -72,7 +72,7 @@ public void testLookupWithFullNameAndResourceRefFalse() throws Exception { } @Test - public void testLookupWithSchemeNameAndResourceRefTrue() throws Exception { + void testLookupWithSchemeNameAndResourceRefTrue() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); Object o = new Object(); jof.setJndiTemplate(new ExpectedLookupTemplate("java:foo", o)); @@ -83,7 +83,7 @@ public void testLookupWithSchemeNameAndResourceRefTrue() throws Exception { } @Test - public void testLookupWithSchemeNameAndResourceRefFalse() throws Exception { + void testLookupWithSchemeNameAndResourceRefFalse() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); Object o = new Object(); jof.setJndiTemplate(new ExpectedLookupTemplate("java:foo", o)); @@ -94,7 +94,7 @@ public void testLookupWithSchemeNameAndResourceRefFalse() throws Exception { } @Test - public void testLookupWithShortNameAndResourceRefTrue() throws Exception { + void testLookupWithShortNameAndResourceRefTrue() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); Object o = new Object(); jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); @@ -105,7 +105,7 @@ public void testLookupWithShortNameAndResourceRefTrue() throws Exception { } @Test - public void testLookupWithShortNameAndResourceRefFalse() throws Exception { + void testLookupWithShortNameAndResourceRefFalse() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); Object o = new Object(); jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); @@ -115,7 +115,7 @@ public void testLookupWithShortNameAndResourceRefFalse() throws Exception { } @Test - public void testLookupWithArbitraryNameAndResourceRefFalse() throws Exception { + void testLookupWithArbitraryNameAndResourceRefFalse() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); Object o = new Object(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", o)); @@ -126,7 +126,7 @@ public void testLookupWithArbitraryNameAndResourceRefFalse() throws Exception { } @Test - public void testLookupWithExpectedTypeAndMatch() throws Exception { + void testLookupWithExpectedTypeAndMatch() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); String s = ""; jof.setJndiTemplate(new ExpectedLookupTemplate("foo", s)); @@ -137,7 +137,7 @@ public void testLookupWithExpectedTypeAndMatch() throws Exception { } @Test - public void testLookupWithExpectedTypeAndNoMatch() throws Exception { + void testLookupWithExpectedTypeAndNoMatch() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", new Object())); jof.setJndiName("foo"); @@ -148,7 +148,7 @@ public void testLookupWithExpectedTypeAndNoMatch() throws Exception { } @Test - public void testLookupWithDefaultObject() throws Exception { + void testLookupWithDefaultObject() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); jof.setJndiName("myFoo"); @@ -159,7 +159,7 @@ public void testLookupWithDefaultObject() throws Exception { } @Test - public void testLookupWithDefaultObjectAndExpectedType() throws Exception { + void testLookupWithDefaultObjectAndExpectedType() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); jof.setJndiName("myFoo"); @@ -170,7 +170,7 @@ public void testLookupWithDefaultObjectAndExpectedType() throws Exception { } @Test - public void testLookupWithDefaultObjectAndExpectedTypeConversion() throws Exception { + void testLookupWithDefaultObjectAndExpectedTypeConversion() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); jof.setJndiName("myFoo"); @@ -181,7 +181,7 @@ public void testLookupWithDefaultObjectAndExpectedTypeConversion() throws Except } @Test - public void testLookupWithDefaultObjectAndExpectedTypeConversionViaBeanFactory() throws Exception { + void testLookupWithDefaultObjectAndExpectedTypeConversionViaBeanFactory() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); jof.setJndiName("myFoo"); @@ -193,7 +193,7 @@ public void testLookupWithDefaultObjectAndExpectedTypeConversionViaBeanFactory() } @Test - public void testLookupWithDefaultObjectAndExpectedTypeNoMatch() throws Exception { + void testLookupWithDefaultObjectAndExpectedTypeNoMatch() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); jof.setJndiName("myFoo"); @@ -203,7 +203,7 @@ public void testLookupWithDefaultObjectAndExpectedTypeNoMatch() throws Exception } @Test - public void testLookupWithProxyInterface() throws Exception { + void testLookupWithProxyInterface() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); TestBean tb = new TestBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); @@ -219,7 +219,7 @@ public void testLookupWithProxyInterface() throws Exception { } @Test - public void testLookupWithProxyInterfaceAndDefaultObject() throws Exception { + void testLookupWithProxyInterfaceAndDefaultObject() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); TestBean tb = new TestBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); @@ -230,7 +230,7 @@ public void testLookupWithProxyInterfaceAndDefaultObject() throws Exception { } @Test - public void testLookupWithProxyInterfaceAndLazyLookup() throws Exception { + void testLookupWithProxyInterfaceAndLazyLookup() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); final TestBean tb = new TestBean(); jof.setJndiTemplate(new JndiTemplate() { @@ -258,7 +258,7 @@ public Object lookup(String name) { } @Test - public void testLookupWithProxyInterfaceWithNotCache() throws Exception { + void testLookupWithProxyInterfaceWithNotCache() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); final TestBean tb = new TestBean(); jof.setJndiTemplate(new JndiTemplate() { @@ -288,7 +288,7 @@ public Object lookup(String name) { } @Test - public void testLookupWithProxyInterfaceWithLazyLookupAndNotCache() throws Exception { + void testLookupWithProxyInterfaceWithLazyLookupAndNotCache() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); final TestBean tb = new TestBean(); jof.setJndiTemplate(new JndiTemplate() { @@ -322,7 +322,7 @@ public Object lookup(String name) { } @Test - public void testLazyLookupWithoutProxyInterface() throws NamingException { + void testLazyLookupWithoutProxyInterface() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiName("foo"); jof.setLookupOnStartup(false); @@ -330,7 +330,7 @@ public void testLazyLookupWithoutProxyInterface() throws NamingException { } @Test - public void testNotCacheWithoutProxyInterface() throws NamingException { + void testNotCacheWithoutProxyInterface() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); jof.setJndiName("foo"); jof.setCache(false); @@ -339,7 +339,7 @@ public void testNotCacheWithoutProxyInterface() throws NamingException { } @Test - public void testLookupWithProxyInterfaceAndExpectedTypeAndMatch() throws Exception { + void testLookupWithProxyInterfaceAndExpectedTypeAndMatch() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); TestBean tb = new TestBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); @@ -356,7 +356,7 @@ public void testLookupWithProxyInterfaceAndExpectedTypeAndMatch() throws Excepti } @Test - public void testLookupWithProxyInterfaceAndExpectedTypeAndNoMatch() { + void testLookupWithProxyInterfaceAndExpectedTypeAndNoMatch() { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); TestBean tb = new TestBean(); jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); @@ -369,7 +369,7 @@ public void testLookupWithProxyInterfaceAndExpectedTypeAndNoMatch() { } @Test - public void testLookupWithExposeAccessContext() throws Exception { + void testLookupWithExposeAccessContext() throws Exception { JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); TestBean tb = new TestBean(); final Context mockCtx = mock(); diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiPropertySourceTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiPropertySourceTests.java index 9ece68cea27b..848f2eade616 100644 --- a/spring-context/src/test/java/org/springframework/jndi/JndiPropertySourceTests.java +++ b/spring-context/src/test/java/org/springframework/jndi/JndiPropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.jndi; import javax.naming.Context; -import javax.naming.NamingException; import org.junit.jupiter.api.Test; @@ -26,28 +25,28 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link JndiPropertySource}. + * Tests for {@link JndiPropertySource}. * * @author Chris Beams * @author Juergen Hoeller * @since 3.1 */ -public class JndiPropertySourceTests { +class JndiPropertySourceTests { @Test - public void nonExistentProperty() { + void nonExistentProperty() { JndiPropertySource ps = new JndiPropertySource("jndiProperties"); assertThat(ps.getProperty("bogus")).isNull(); } @Test - public void nameBoundWithoutPrefix() { + void nameBoundWithoutPrefix() { final SimpleNamingContext context = new SimpleNamingContext(); context.bind("p1", "v1"); JndiTemplate jndiTemplate = new JndiTemplate() { @Override - protected Context createInitialContext() throws NamingException { + protected Context createInitialContext() { return context; } }; @@ -60,13 +59,13 @@ protected Context createInitialContext() throws NamingException { } @Test - public void nameBoundWithPrefix() { + void nameBoundWithPrefix() { final SimpleNamingContext context = new SimpleNamingContext(); context.bind("java:comp/env/p1", "v1"); JndiTemplate jndiTemplate = new JndiTemplate() { @Override - protected Context createInitialContext() throws NamingException { + protected Context createInitialContext() { return context; } }; @@ -79,10 +78,10 @@ protected Context createInitialContext() throws NamingException { } @Test - public void propertyWithDefaultClauseInResourceRefMode() { + void propertyWithDefaultClauseInResourceRefMode() { JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate() { @Override - public Object lookup(String jndiName) throws NamingException { + public Object lookup(String jndiName) { throw new IllegalStateException("Should not get called"); } }; @@ -93,10 +92,10 @@ public Object lookup(String jndiName) throws NamingException { } @Test - public void propertyWithColonInNonResourceRefMode() { + void propertyWithColonInNonResourceRefMode() { JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate() { @Override - public Object lookup(String jndiName) throws NamingException { + public Object lookup(String jndiName) { assertThat(jndiName).isEqualTo("my:key"); return "my:value"; } diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiTemplateEditorTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateEditorTests.java index eaeb25836b17..3343fc1d5058 100644 --- a/spring-context/src/test/java/org/springframework/jndi/JndiTemplateEditorTests.java +++ b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,16 +25,16 @@ * @author Rod Johnson * @author Chris Beams */ -public class JndiTemplateEditorTests { +class JndiTemplateEditorTests { @Test - public void testNullIsIllegalArgument() { + void testNullIsIllegalArgument() { assertThatIllegalArgumentException().isThrownBy(() -> new JndiTemplateEditor().setAsText(null)); } @Test - public void testEmptyStringMeansNullEnvironment() { + void testEmptyStringMeansNullEnvironment() { JndiTemplateEditor je = new JndiTemplateEditor(); je.setAsText(""); JndiTemplate jt = (JndiTemplate) je.getValue(); @@ -42,7 +42,7 @@ public void testEmptyStringMeansNullEnvironment() { } @Test - public void testCustomEnvironment() { + void testCustomEnvironment() { JndiTemplateEditor je = new JndiTemplateEditor(); // These properties are meaningless for JNDI, but we don't worry about that: // the underlying JNDI implementation will throw exceptions when the user tries diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiTemplateTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateTests.java index a61b632735f6..b6db8ea6dca6 100644 --- a/spring-context/src/test/java/org/springframework/jndi/JndiTemplateTests.java +++ b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,10 @@ * @author Chris Beams * @since 08.07.2003 */ -public class JndiTemplateTests { +class JndiTemplateTests { @Test - public void testLookupSucceeds() throws Exception { + void testLookupSucceeds() throws Exception { Object o = new Object(); String name = "foo"; final Context context = mock(); @@ -55,7 +55,7 @@ protected Context createInitialContext() { } @Test - public void testLookupFails() throws Exception { + void testLookupFails() throws Exception { NameNotFoundException ne = new NameNotFoundException(); String name = "foo"; final Context context = mock(); @@ -74,7 +74,7 @@ protected Context createInitialContext() { } @Test - public void testLookupReturnsNull() throws Exception { + void testLookupReturnsNull() throws Exception { String name = "foo"; final Context context = mock(); given(context.lookup(name)).willReturn(null); @@ -92,7 +92,7 @@ protected Context createInitialContext() { } @Test - public void testLookupFailsWithTypeMismatch() throws Exception { + void testLookupFailsWithTypeMismatch() throws Exception { Object o = new Object(); String name = "foo"; final Context context = mock(); @@ -111,7 +111,7 @@ protected Context createInitialContext() { } @Test - public void testBind() throws Exception { + void testBind() throws Exception { Object o = new Object(); String name = "foo"; final Context context = mock(); @@ -129,7 +129,7 @@ protected Context createInitialContext() { } @Test - public void testRebind() throws Exception { + void testRebind() throws Exception { Object o = new Object(); String name = "foo"; final Context context = mock(); @@ -147,7 +147,7 @@ protected Context createInitialContext() { } @Test - public void testUnbind() throws Exception { + void testUnbind() throws Exception { String name = "something"; final Context context = mock(); diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java index a7f335afa39f..aa83482bea0e 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,12 +25,12 @@ /** - * Unit tests for {@link AnnotationAsyncExecutionInterceptor}. + * Tests for {@link AnnotationAsyncExecutionInterceptor}. * * @author Chris Beams * @since 3.1.2 */ -public class AnnotationAsyncExecutionInterceptorTests { +class AnnotationAsyncExecutionInterceptorTests { @Test @SuppressWarnings("unused") diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java index 3d342f0e478e..9302682f690b 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,10 +49,10 @@ * @author Juergen Hoeller * @author Stephane Nicoll */ -public class AsyncAnnotationBeanPostProcessorTests { +class AsyncAnnotationBeanPostProcessorTests { @Test - public void proxyCreated() { + void proxyCreated() { ConfigurableApplicationContext context = initContext( new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); Object target = context.getBean("target"); @@ -61,7 +61,7 @@ public void proxyCreated() { } @Test - public void invokedAsynchronously() { + void invokedAsynchronously() { ConfigurableApplicationContext context = initContext( new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); @@ -75,7 +75,7 @@ public void invokedAsynchronously() { } @Test - public void invokedAsynchronouslyOnProxyTarget() { + void invokedAsynchronouslyOnProxyTarget() { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("postProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); TestBean tb = new TestBean(); @@ -94,7 +94,7 @@ public void invokedAsynchronouslyOnProxyTarget() { } @Test - public void threadNamePrefix() { + void threadNamePrefix() { BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setThreadNamePrefix("testExecutor"); @@ -111,7 +111,7 @@ public void threadNamePrefix() { } @Test - public void taskExecutorByBeanType() { + void taskExecutorByBeanType() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); @@ -136,7 +136,7 @@ public void taskExecutorByBeanType() { } @Test - public void taskExecutorByBeanName() { + void taskExecutorByBeanName() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); @@ -165,7 +165,7 @@ public void taskExecutorByBeanName() { } @Test - public void configuredThroughNamespace() { + void configuredThroughNamespace() { GenericXmlApplicationContext context = new GenericXmlApplicationContext(); context.load(new ClassPathResource("taskNamespaceTests.xml", getClass())); context.refresh(); @@ -223,7 +223,7 @@ private void assertFutureWithException(Future result, } @Test - public void handleExceptionWithCustomExceptionHandler() { + void handleExceptionWithCustomExceptionHandler() { Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(); @@ -240,7 +240,7 @@ public void handleExceptionWithCustomExceptionHandler() { } @Test - public void exceptionHandlerThrowsUnexpectedException() { + void exceptionHandlerThrowsUnexpectedException() { Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(true); diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java index a97d1a9edf68..074dd0ce971f 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -329,7 +329,7 @@ void dynamicAsyncMethodsInInterfaceWithPostProcessor() throws Exception { } @Test - void asyncMethodListener() throws Exception { + void asyncMethodListener() { // Arrange GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodListener.class)); @@ -339,14 +339,14 @@ void asyncMethodListener() throws Exception { context.refresh(); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> listenerCalled == 1); context.close(); } @Test - void asyncClassListener() throws Exception { + void asyncClassListener() { // Arrange GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassListener.class)); @@ -357,14 +357,14 @@ void asyncClassListener() throws Exception { context.close(); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> listenerCalled == 2); assertThat(listenerConstructed).isEqualTo(1); } @Test - void asyncPrototypeClassListener() throws Exception { + void asyncPrototypeClassListener() { // Arrange GenericApplicationContext context = new GenericApplicationContext(); RootBeanDefinition listenerDef = new RootBeanDefinition(AsyncClassListener.class); @@ -377,7 +377,7 @@ void asyncPrototypeClassListener() throws Exception { context.close(); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> listenerCalled == 2); assertThat(listenerConstructed).isEqualTo(2); diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java index b37eca9eff52..f8b9849a2780 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ /** * @author Juergen Hoeller */ -public class AsyncResultTests { +class AsyncResultTests { @Test @SuppressWarnings("deprecation") @@ -37,7 +37,7 @@ public void asyncResultWithCallbackAndValue() throws Exception { String value = "val"; final Set values = new HashSet<>(1); org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forValue(value); - future.addCallback(new org.springframework.util.concurrent.ListenableFutureCallback() { + future.addCallback(new org.springframework.util.concurrent.ListenableFutureCallback<>() { @Override public void onSuccess(String result) { values.add(result); @@ -47,7 +47,7 @@ public void onFailure(Throwable ex) { throw new AssertionError("Failure callback not expected: " + ex, ex); } }); - assertThat(values.iterator().next()).isSameAs(value); + assertThat(values).singleElement().isSameAs(value); assertThat(future.get()).isSameAs(value); assertThat(future.completable().get()).isSameAs(value); future.completable().thenAccept(v -> assertThat(v).isSameAs(value)); @@ -55,11 +55,11 @@ public void onFailure(Throwable ex) { @Test @SuppressWarnings("deprecation") - public void asyncResultWithCallbackAndException() throws Exception { + public void asyncResultWithCallbackAndException() { IOException ex = new IOException(); final Set values = new HashSet<>(1); org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forExecutionException(ex); - future.addCallback(new org.springframework.util.concurrent.ListenableFutureCallback() { + future.addCallback(new org.springframework.util.concurrent.ListenableFutureCallback<>() { @Override public void onSuccess(String result) { throw new AssertionError("Success callback not expected: " + result); @@ -69,7 +69,7 @@ public void onFailure(Throwable ex) { values.add(ex); } }); - assertThat(values.iterator().next()).isSameAs(ex); + assertThat(values).singleElement().isSameAs(ex); assertThatExceptionOfType(ExecutionException.class) .isThrownBy(future::get) .withCause(ex); @@ -85,7 +85,7 @@ public void asyncResultWithSeparateCallbacksAndValue() throws Exception { final Set values = new HashSet<>(1); org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forValue(value); future.addCallback(values::add, ex -> new AssertionError("Failure callback not expected: " + ex)); - assertThat(values.iterator().next()).isSameAs(value); + assertThat(values).singleElement().isSameAs(value); assertThat(future.get()).isSameAs(value); assertThat(future.completable().get()).isSameAs(value); future.completable().thenAccept(v -> assertThat(v).isSameAs(value)); @@ -93,12 +93,12 @@ public void asyncResultWithSeparateCallbacksAndValue() throws Exception { @Test @SuppressWarnings("deprecation") - public void asyncResultWithSeparateCallbacksAndException() throws Exception { + public void asyncResultWithSeparateCallbacksAndException() { IOException ex = new IOException(); final Set values = new HashSet<>(1); org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forExecutionException(ex); future.addCallback(result -> new AssertionError("Success callback not expected: " + result), values::add); - assertThat(values.iterator().next()).isSameAs(ex); + assertThat(values).singleElement().isSameAs(ex); assertThatExceptionOfType(ExecutionException.class) .isThrownBy(future::get) .withCause(ex); diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java index 40d79675b25b..1df89b1d03d0 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; @@ -65,10 +66,10 @@ * @author Stephane Nicoll * @since 3.1 */ -public class EnableAsyncTests { +class EnableAsyncTests { @Test - public void proxyingOccurs() { + void proxyingOccurs() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncConfig.class); ctx.refresh(); @@ -80,7 +81,7 @@ public void proxyingOccurs() { } @Test - public void proxyingOccursWithMockitoStub() { + void proxyingOccursWithMockitoStub() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncConfigWithMockito.class, AsyncBeanUser.class); ctx.refresh(); @@ -93,7 +94,7 @@ public void proxyingOccursWithMockitoStub() { } @Test - public void properExceptionForExistingProxyDependencyMismatch() { + void properExceptionForExistingProxyDependencyMismatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncConfig.class, AsyncBeanWithInterface.class, AsyncBeanUser.class); assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(ctx::refresh) @@ -102,7 +103,7 @@ public void properExceptionForExistingProxyDependencyMismatch() { } @Test - public void properExceptionForResolvedProxyDependencyMismatch() { + void properExceptionForResolvedProxyDependencyMismatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncConfig.class, AsyncBeanUser.class, AsyncBeanWithInterface.class); assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(ctx::refresh) @@ -111,7 +112,7 @@ public void properExceptionForResolvedProxyDependencyMismatch() { } @Test - public void withAsyncBeanWithExecutorQualifiedByName() throws ExecutionException, InterruptedException { + void withAsyncBeanWithExecutorQualifiedByName() throws ExecutionException, InterruptedException { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncWithExecutorQualifiedByNameConfig.class); ctx.refresh(); @@ -130,7 +131,7 @@ public void withAsyncBeanWithExecutorQualifiedByName() throws ExecutionException } @Test - public void withAsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder() throws Exception { + void withAsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder() throws Exception { System.setProperty("myExecutor", "myExecutor1"); System.setProperty("my.app.myExecutor", "myExecutor2"); @@ -140,13 +141,18 @@ public void withAsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder() throws context.getBean(AsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder.class); Future workerThread1 = asyncBean.myWork1(); - assertThat(workerThread1.get().getName()).startsWith("myExecutor1-"); + assertThat(workerThread1.get(100, TimeUnit.MILLISECONDS).getName()).startsWith("myExecutor1-"); + context.stop(); Future workerThread2 = asyncBean.myWork2(); - assertThat(workerThread2.get().getName()).startsWith("myExecutor2-"); + assertThatExceptionOfType(TimeoutException.class).isThrownBy( + () -> workerThread2.get(100, TimeUnit.MILLISECONDS)); + + context.start(); + assertThat(workerThread2.get(100, TimeUnit.MILLISECONDS).getName()).startsWith("myExecutor2-"); Future workerThread3 = asyncBean.fallBackToDefaultExecutor(); - assertThat(workerThread3.get().getName()).startsWith("SimpleAsyncTaskExecutor"); + assertThat(workerThread3.get(100, TimeUnit.MILLISECONDS).getName()).startsWith("SimpleAsyncTaskExecutor"); } finally { System.clearProperty("myExecutor"); @@ -155,7 +161,7 @@ public void withAsyncBeanWithExecutorQualifiedByExpressionOrPlaceholder() throws } @Test - public void asyncProcessorIsOrderedLowestPrecedenceByDefault() { + void asyncProcessorIsOrderedLowestPrecedenceByDefault() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncConfig.class); ctx.refresh(); @@ -167,7 +173,7 @@ public void asyncProcessorIsOrderedLowestPrecedenceByDefault() { } @Test - public void orderAttributeIsPropagated() { + void orderAttributeIsPropagated() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(OrderedAsyncConfig.class); ctx.refresh(); @@ -179,7 +185,7 @@ public void orderAttributeIsPropagated() { } @Test - public void customAsyncAnnotationIsPropagated() { + void customAsyncAnnotationIsPropagated() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomAsyncAnnotationConfig.class, CustomAsyncBean.class); ctx.refresh(); @@ -202,7 +208,7 @@ public void customAsyncAnnotationIsPropagated() { * Fails with classpath errors on trying to classload AnnotationAsyncExecutionAspect. */ @Test - public void aspectModeAspectJAttemptsToRegisterAsyncAspect() { + void aspectModeAspectJAttemptsToRegisterAsyncAspect() { @SuppressWarnings("resource") AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AspectJAsyncAnnotationConfig.class); @@ -210,7 +216,7 @@ public void aspectModeAspectJAttemptsToRegisterAsyncAspect() { } @Test - public void customExecutorBean() { + void customExecutorBean() { // Arrange AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomExecutorBean.class); @@ -220,7 +226,7 @@ public void customExecutorBean() { asyncBean.work(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> asyncBean.getThreadOfExecution() != null); assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); @@ -228,7 +234,7 @@ public void customExecutorBean() { } @Test - public void customExecutorConfig() { + void customExecutorConfig() { // Arrange AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomExecutorConfig.class); @@ -238,7 +244,7 @@ public void customExecutorConfig() { asyncBean.work(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> asyncBean.getThreadOfExecution() != null); assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); @@ -246,7 +252,7 @@ public void customExecutorConfig() { } @Test - public void customExecutorConfigWithThrowsException() { + void customExecutorConfigWithThrowsException() { // Arrange AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomExecutorConfig.class); @@ -260,14 +266,14 @@ public void customExecutorConfigWithThrowsException() { asyncBean.fail(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .untilAsserted(() -> exceptionHandler.assertCalledWith(method, UnsupportedOperationException.class)); ctx.close(); } @Test - public void customExecutorBeanConfig() { + void customExecutorBeanConfig() { // Arrange AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomExecutorBeanConfig.class, ExecutorPostProcessor.class); @@ -277,7 +283,7 @@ public void customExecutorBeanConfig() { asyncBean.work(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> asyncBean.getThreadOfExecution() != null); assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Post-"); @@ -285,7 +291,7 @@ public void customExecutorBeanConfig() { } @Test - public void customExecutorBeanConfigWithThrowsException() { + void customExecutorBeanConfigWithThrowsException() { // Arrange AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomExecutorBeanConfig.class, ExecutorPostProcessor.class); @@ -299,7 +305,7 @@ public void customExecutorBeanConfigWithThrowsException() { asyncBean.fail(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .untilAsserted(() -> exceptionHandler.assertCalledWith(method, UnsupportedOperationException.class)); ctx.close(); @@ -314,7 +320,7 @@ public void findOnInterfaceWithInterfaceProxy() { asyncBean.work(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> asyncBean.getThreadOfExecution() != null); assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); @@ -330,7 +336,7 @@ public void findOnInterfaceWithCglibProxy() { asyncBean.work(); // Assert Awaitility.await() - .atMost(500, TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> asyncBean.getThreadOfExecution() != null); assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java index 11854b11e30f..0334d066b2e1 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,16 +20,30 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import jakarta.annotation.PreDestroy; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.task.TaskExecutor; import org.springframework.core.testfixture.EnabledForTestGroups; import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.IntervalTask; import org.springframework.scheduling.config.ScheduledTaskHolder; @@ -44,85 +58,167 @@ * * @author Chris Beams * @author Sam Brannen + * @author Juergen Hoeller * @since 3.1 */ -public class EnableSchedulingTests { +class EnableSchedulingTests { private AnnotationConfigApplicationContext ctx; + private static final AtomicBoolean shutdownFailure = new AtomicBoolean(); + + + @BeforeEach + void reset() { + shutdownFailure.set(false); + } @AfterEach - public void tearDown() { + void tearDown() { if (ctx != null) { ctx.close(); } + assertThat(shutdownFailure).isFalse(); } - @Test + /* + * Tests compatibility between default executor in TaskSchedulerRouter + * and explicit ThreadPoolTaskScheduler in configuration subclass. + */ + @ParameterizedTest + @ValueSource(classes = {FixedRateTaskConfig.class, FixedRateTaskConfigSubclass.class}) @EnabledForTestGroups(LONG_RUNNING) - public void withFixedRateTask() throws InterruptedException { - ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig.class); + void withFixedRateTask(Class configClass) throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(configClass); assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2); Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); } + /* + * Tests compatibility between SimpleAsyncTaskScheduler in regular configuration + * and explicit ThreadPoolTaskScheduler in configuration subclass. This includes + * pause/resume behavior and a controlled shutdown with a 1s termination timeout. + */ + @ParameterizedTest + @ValueSource(classes = {ExplicitSchedulerConfig.class, ExplicitSchedulerConfigSubclass.class}) + @Timeout(2) // should actually complete within 1s + @EnabledForTestGroups(LONG_RUNNING) + void withExplicitScheduler(Class configClass) throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(configClass); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); + + Thread.sleep(110); + ctx.stop(); + int count1 = ctx.getBean(AtomicInteger.class).get(); + assertThat(count1).isGreaterThanOrEqualTo(10).isLessThan(20); + Thread.sleep(110); + int count2 = ctx.getBean(AtomicInteger.class).get(); + assertThat(count2).isGreaterThanOrEqualTo(10).isLessThan(20); + ctx.start(); + Thread.sleep(110); + int count3 = ctx.getBean(AtomicInteger.class).get(); + assertThat(count3).isGreaterThanOrEqualTo(20); + + TaskExecutor executor = ctx.getBean(TaskExecutor.class); + AtomicInteger count = new AtomicInteger(0); + for (int i = 0; i < 2; i++) { + executor.execute(() -> { + try { + Thread.sleep(10000); // try to break test timeout + } + catch (InterruptedException ex) { + // expected during executor shutdown + try { + Thread.sleep(500); + // should get here within task termination timeout (1000) + count.incrementAndGet(); + } + catch (InterruptedException ex2) { + // not expected + } + } + }); + } + + assertThat(ctx.getBean(ExplicitSchedulerConfig.class).threadName).startsWith("explicitScheduler-"); + assertThat(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler")) + .contains(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + + // Include executor shutdown in test timeout (2 seconds), + // expecting interruption of the sleeping thread... + ctx.close(); + assertThat(count.intValue()).isEqualTo(2); + } + + @Test + void withExplicitSchedulerAmbiguity_andSchedulingEnabled() { + // No exception raised as of 4.3, aligned with the behavior for @Async methods (SPR-14030) + ctx = new AnnotationConfigApplicationContext(AmbiguousExplicitSchedulerConfig.class); + } + @Test @EnabledForTestGroups(LONG_RUNNING) - public void withSubclass() throws InterruptedException { - ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfigSubclass.class); - assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2); + void withExplicitScheduledTaskRegistrar() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(ExplicitScheduledTaskRegistrarConfig.class); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); + assertThat(ctx.getBean(ExplicitScheduledTaskRegistrarConfig.class).threadName).startsWith("explicitScheduler1"); } @Test @EnabledForTestGroups(LONG_RUNNING) - public void withExplicitScheduler() throws InterruptedException { - ctx = new AnnotationConfigApplicationContext(ExplicitSchedulerConfig.class); + void withQualifiedScheduler() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfig.class); assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); - assertThat(ctx.getBean(ExplicitSchedulerConfig.class).threadName).startsWith("explicitScheduler-"); - assertThat(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler")).contains( - TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(ctx.getBean(QualifiedExplicitSchedulerConfig.class).threadName).startsWith("explicitScheduler1"); } @Test - public void withExplicitSchedulerAmbiguity_andSchedulingEnabled() { - // No exception raised as of 4.3, aligned with the behavior for @Async methods (SPR-14030) - ctx = new AnnotationConfigApplicationContext(AmbiguousExplicitSchedulerConfig.class); + @EnabledForTestGroups(LONG_RUNNING) + void withQualifiedSchedulerAndPlaceholder() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfigWithPlaceholder.class); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); + + Thread.sleep(110); + assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); + assertThat(ctx.getBean(QualifiedExplicitSchedulerConfigWithPlaceholder.class).threadName) + .startsWith("explicitScheduler1").isNotEqualTo("explicitScheduler1-1"); } @Test @EnabledForTestGroups(LONG_RUNNING) - public void withExplicitScheduledTaskRegistrar() throws InterruptedException { - ctx = new AnnotationConfigApplicationContext(ExplicitScheduledTaskRegistrarConfig.class); + void withQualifiedSchedulerWithFixedDelayTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfigWithFixedDelayTask.class); assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); Thread.sleep(110); - assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); - assertThat(ctx.getBean(ExplicitScheduledTaskRegistrarConfig.class).threadName).startsWith("explicitScheduler1"); + assertThat(ctx.getBean(AtomicInteger.class).get()).isBetween(4, 5); + assertThat(ctx.getBean(QualifiedExplicitSchedulerConfigWithFixedDelayTask.class).threadName) + .isEqualTo("explicitScheduler1-1"); } @Test - public void withAmbiguousTaskSchedulers_butNoActualTasks() { + void withAmbiguousTaskSchedulers_butNoActualTasks() { ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withAmbiguousTaskSchedulers_butNoActualTasks.class); } @Test - public void withAmbiguousTaskSchedulers_andSingleTask() { + void withAmbiguousTaskSchedulers_andSingleTask() { // No exception raised as of 4.3, aligned with the behavior for @Async methods (SPR-14030) ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask.class); } @Test @EnabledForTestGroups(LONG_RUNNING) - public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrarBean() throws InterruptedException { + void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrarBean() throws InterruptedException { ctx = new AnnotationConfigApplicationContext( SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrar.class); @@ -132,7 +228,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTa @Test @EnabledForTestGroups(LONG_RUNNING) - public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute() throws InterruptedException { + void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute() throws InterruptedException { ctx = new AnnotationConfigApplicationContext( SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute.class); @@ -142,7 +238,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNa @Test @EnabledForTestGroups(LONG_RUNNING) - public void withTaskAddedVia_configureTasks() throws InterruptedException { + void withTaskAddedVia_configureTasks() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withTaskAddedVia_configureTasks.class); Thread.sleep(110); @@ -151,24 +247,77 @@ public void withTaskAddedVia_configureTasks() throws InterruptedException { @Test @EnabledForTestGroups(LONG_RUNNING) - public void withTriggerTask() throws InterruptedException { - ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class); + void withInitiallyDelayedFixedRateTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig_withInitialDelay.class); - Thread.sleep(110); - assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThan(1); + Thread.sleep(1950); + AtomicInteger counter = ctx.getBean(AtomicInteger.class); + + // The @Scheduled method should have been called several times + // but not more times than the delay allows. + assertThat(counter.get()).isBetween(6, 10); } @Test @EnabledForTestGroups(LONG_RUNNING) - public void withInitiallyDelayedFixedRateTask() throws InterruptedException { - ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig_withInitialDelay.class); + void withInitiallyDelayedFixedDelayTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withInitialDelay.class); Thread.sleep(1950); AtomicInteger counter = ctx.getBean(AtomicInteger.class); - // The @Scheduled method should have been called at least once but - // not more times than the delay allows. - assertThat(counter.get()).isBetween(1, 10); + // The @Scheduled method should have been called several times + // but not more times than the delay allows. + assertThat(counter.get()).isBetween(1, 5); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void withPrototypeContainedFixedDelayTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withPrototypeBean.class); + + ctx.getBean(PrototypeBeanWithScheduled.class); + Thread.sleep(1950); + AtomicInteger counter = ctx.getBean(AtomicInteger.class); + + // The @Scheduled method should have been called several times + // but not more times than the delay allows. + assertThat(counter.get()).isBetween(1, 5); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void withPrototypeFactoryContainedFixedDelayTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withFactoryBean.class); + + ctx.getBean(PrototypeBeanWithScheduled.class); + Thread.sleep(1950); + AtomicInteger counter = ctx.getBean(AtomicInteger.class); + + // The @Scheduled method should have been called several times + // but not more times than the delay allows. + assertThat(counter.get()).isBetween(1, 5); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void withOneTimeTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(OneTimeTaskConfig.class); + + Thread.sleep(110); + AtomicInteger counter = ctx.getBean(AtomicInteger.class); + + // The @Scheduled method should have been called exactly once. + assertThat(counter.get()).isEqualTo(1); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void withTriggerTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class); + + Thread.sleep(110); + assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThan(1); } @@ -195,6 +344,11 @@ public void task() { @Configuration static class FixedRateTaskConfigSubclass extends FixedRateTaskConfig { + + @Bean + public TaskScheduler taskScheduler() { + return new ThreadPoolTaskScheduler(); + } } @@ -206,8 +360,9 @@ static class ExplicitSchedulerConfig { @Bean public TaskScheduler myTaskScheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); scheduler.setThreadNamePrefix("explicitScheduler-"); + scheduler.setTaskTerminationTimeout(1000); return scheduler; } @@ -224,21 +379,37 @@ public void task() { } + @Configuration + static class ExplicitSchedulerConfigSubclass extends ExplicitSchedulerConfig { + + @Bean + @Override + public TaskScheduler myTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(2); + scheduler.setThreadNamePrefix("explicitScheduler-"); + scheduler.setAcceptTasksAfterContextClose(true); + scheduler.setAwaitTerminationMillis(1000); + return scheduler; + } + } + + @Configuration @EnableScheduling static class AmbiguousExplicitSchedulerConfig { @Bean public TaskScheduler taskScheduler1() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler1"); + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); return scheduler; } @Bean public TaskScheduler taskScheduler2() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler2"); + scheduler.setThreadNamePrefix("explicitScheduler2-"); return scheduler; } @@ -256,15 +427,15 @@ static class ExplicitScheduledTaskRegistrarConfig implements SchedulingConfigure @Bean public TaskScheduler taskScheduler1() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler1"); + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); return scheduler; } @Bean public TaskScheduler taskScheduler2() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler2"); + scheduler.setThreadNamePrefix("explicitScheduler2-"); return scheduler; } @@ -288,19 +459,130 @@ public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { @Configuration @EnableScheduling - static class SchedulingEnabled_withAmbiguousTaskSchedulers_butNoActualTasks { + static class QualifiedExplicitSchedulerConfig { + + String threadName; + + @Bean @Qualifier("myScheduler") + public TaskScheduler taskScheduler1() { + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2-"); + return scheduler; + } + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(fixedRate = 10, scheduler = "myScheduler") + public void task() throws InterruptedException { + threadName = Thread.currentThread().getName(); + counter().incrementAndGet(); + Thread.sleep(10); + } + } + + + @Configuration + @EnableScheduling + static class QualifiedExplicitSchedulerConfigWithPlaceholder { + + String threadName; + + @Bean @Qualifier("myScheduler") + public TaskScheduler taskScheduler1() { + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2-"); + return scheduler; + } + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(fixedRate = 10, scheduler = "${scheduler}") + public void task() throws InterruptedException { + threadName = Thread.currentThread().getName(); + counter().incrementAndGet(); + Thread.sleep(10); + } @Bean + public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { + PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("scheduler", "myScheduler"); + pspc.setProperties(props); + return pspc; + } + } + + + @Configuration + @EnableScheduling + static class QualifiedExplicitSchedulerConfigWithFixedDelayTask { + + String threadName; + + @Bean @Qualifier("myScheduler") public TaskScheduler taskScheduler1() { + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler1"); + scheduler.setThreadNamePrefix("explicitScheduler2-"); + return scheduler; + } + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(fixedDelay = 10, scheduler = "myScheduler") + public void task() throws InterruptedException { + threadName = Thread.currentThread().getName(); + counter().incrementAndGet(); + Thread.sleep(10); + } + } + + + @Configuration + @EnableScheduling + static class SchedulingEnabled_withAmbiguousTaskSchedulers_butNoActualTasks { + + @Bean + public TaskScheduler taskScheduler1() { + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); return scheduler; } @Bean public TaskScheduler taskScheduler2() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler2"); + scheduler.setThreadNamePrefix("explicitScheduler2-"); return scheduler; } } @@ -316,15 +598,16 @@ public void task() { @Bean public TaskScheduler taskScheduler1() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler1"); + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); + scheduler.setConcurrencyLimit(1); return scheduler; } @Bean public TaskScheduler taskScheduler2() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix("explicitScheduler2"); + scheduler.setThreadNamePrefix("explicitScheduler2-"); return scheduler; } } @@ -357,8 +640,9 @@ public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { @Bean public TaskScheduler taskScheduler1() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); scheduler.setThreadNamePrefix("explicitScheduler1-"); + scheduler.setConcurrencyLimit(1); return scheduler; } @@ -387,8 +671,9 @@ public ThreadAwareWorker worker() { @Bean public TaskScheduler taskScheduler1() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); scheduler.setThreadNamePrefix("explicitScheduler1-"); + scheduler.setConcurrencyLimit(1); return scheduler; } @@ -431,37 +716,179 @@ public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { @Configuration - static class TriggerTaskConfig { + @EnableScheduling + static class FixedRateTaskConfig_withInitialDelay { + + @Autowired + ScheduledAnnotationBeanPostProcessor bpp; @Bean public AtomicInteger counter() { return new AtomicInteger(); } + @Scheduled(initialDelay = 1000, fixedRate = 100) + public void task() throws InterruptedException { + counter().incrementAndGet(); + Thread.sleep(100); + } + + @PreDestroy + public void validateLateCancellation() { + if (this.bpp.getScheduledTasks().isEmpty()) { + shutdownFailure.set(true); + } + } + } + + + @Configuration + @EnableScheduling + static class FixedDelayTaskConfig_withInitialDelay { + + @Autowired + ScheduledAnnotationBeanPostProcessor bpp; + @Bean - public TaskScheduler scheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.initialize(); - scheduler.schedule(() -> counter().incrementAndGet(), - triggerContext -> Instant.now().plus(10, ChronoUnit.MILLIS)); - return scheduler; + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(initialDelay = 1000, fixedDelay = 100) + public void task() throws InterruptedException { + counter().incrementAndGet(); + Thread.sleep(100); + } + + @PreDestroy + public void validateLateCancellation() { + if (this.bpp.getScheduledTasks().isEmpty()) { + shutdownFailure.set(true); + } } } @Configuration @EnableScheduling - static class FixedRateTaskConfig_withInitialDelay { + static class FixedDelayTaskConfig_withPrototypeBean { + + @Autowired + ScheduledAnnotationBeanPostProcessor bpp; @Bean public AtomicInteger counter() { return new AtomicInteger(); } - @Scheduled(initialDelay = 1000, fixedRate = 100) + @Bean @Scope("prototype") + public PrototypeBeanWithScheduled prototypeBean() { + return new PrototypeBeanWithScheduled(counter()); + } + + @PreDestroy + public void validateEarlyCancellation() { + if (!this.bpp.getScheduledTasks().isEmpty()) { + shutdownFailure.set(true); + } + } + } + + + @Configuration + @EnableScheduling + static class FixedDelayTaskConfig_withFactoryBean { + + @Autowired + ScheduledAnnotationBeanPostProcessor bpp; + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Bean + public FactoryBeanForScheduled prototypeBean() { + return new FactoryBeanForScheduled(counter()); + } + + @PreDestroy + public void validateEarlyCancellation() { + if (!this.bpp.getScheduledTasks().isEmpty()) { + shutdownFailure.set(true); + } + } + } + + + static class PrototypeBeanWithScheduled { + + private AtomicInteger counter; + + public PrototypeBeanWithScheduled(AtomicInteger counter) { + this.counter = counter; + } + + @Scheduled(initialDelay = 1000, fixedDelay = 100) + public void task() throws InterruptedException { + this.counter.incrementAndGet(); + Thread.sleep(100); + } + } + + + static class FactoryBeanForScheduled implements FactoryBean { + + private AtomicInteger counter; + + public FactoryBeanForScheduled(AtomicInteger counter) { + this.counter = counter; + } + + @Override + public PrototypeBeanWithScheduled getObject() { + return new PrototypeBeanWithScheduled(this.counter); + } + + @Override + public Class getObjectType() { + return PrototypeBeanWithScheduled.class; + } + } + + + @Configuration + @EnableScheduling + static class OneTimeTaskConfig { + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(initialDelay = 10) public void task() { counter().incrementAndGet(); } } + + @Configuration + static class TriggerTaskConfig { + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Bean + public TaskScheduler scheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + scheduler.schedule(() -> counter().incrementAndGet(), + triggerContext -> Instant.now().plus(10, ChronoUnit.MILLIS)); + return scheduler; + } + } + } diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorObservabilityTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorObservabilityTests.java new file mode 100644 index 000000000000..6132b222e83b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorObservabilityTests.java @@ -0,0 +1,291 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.annotation; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import reactor.core.observability.DefaultSignalListener; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.scheduling.config.ScheduledTask; +import org.springframework.scheduling.config.ScheduledTaskHolder; +import org.springframework.scheduling.support.ScheduledTaskObservationContext; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Observability tests for {@link ScheduledAnnotationBeanPostProcessor}. + * + * @author Brian Clozel + */ +class ScheduledAnnotationBeanPostProcessorObservabilityTests { + + private final StaticApplicationContext context = new StaticApplicationContext(); + + private final SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + + private final TestObservationRegistry observationRegistry = TestObservationRegistry.create(); + + + @AfterEach + void closeContext() { + context.close(); + } + + + @Test + void shouldRecordSuccessObservationsForTasks() throws Exception { + registerScheduledBean(FixedDelayBean.class); + runScheduledTaskAndAwait(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") + .hasLowCardinalityKeyValue("code.function", "fixedDelay") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".FixedDelayBean") + .hasLowCardinalityKeyValue("exception", "none"); + } + + @Test + void shouldRecordFailureObservationsForTasksThrowing() throws Exception { + registerScheduledBean(FixedDelayErrorBean.class); + runScheduledTaskAndAwait(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "ERROR") + .hasLowCardinalityKeyValue("code.function", "error") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".FixedDelayErrorBean") + .hasLowCardinalityKeyValue("exception", "IllegalStateException"); + } + + @Test + void shouldRecordSuccessObservationsForReactiveTasks() throws Exception { + registerScheduledBean(FixedDelayReactiveBean.class); + runScheduledTaskAndAwait(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") + .hasLowCardinalityKeyValue("code.function", "fixedDelay") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".FixedDelayReactiveBean") + .hasLowCardinalityKeyValue("exception", "none"); + } + + @Test + void shouldRecordFailureObservationsForReactiveTasksThrowing() throws Exception { + registerScheduledBean(FixedDelayReactiveErrorBean.class); + runScheduledTaskAndAwait(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "ERROR") + .hasLowCardinalityKeyValue("code.function", "error") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".FixedDelayReactiveErrorBean") + .hasLowCardinalityKeyValue("exception", "IllegalStateException"); + } + + @Test + void shouldRecordCancelledObservationsForTasks() throws Exception { + registerScheduledBean(CancelledTaskBean.class); + ScheduledTask scheduledTask = getScheduledTask(); + this.taskExecutor.execute(scheduledTask.getTask().getRunnable()); + context.getBean(TaskTester.class).await(); + scheduledTask.cancel(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN") + .hasLowCardinalityKeyValue("code.function", "cancelled") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".CancelledTaskBean") + .hasLowCardinalityKeyValue("exception", "none"); + } + + @Test + void shouldRecordCancelledObservationsForReactiveTasks() throws Exception { + registerScheduledBean(CancelledReactiveTaskBean.class); + ScheduledTask scheduledTask = getScheduledTask(); + this.taskExecutor.execute(scheduledTask.getTask().getRunnable()); + context.getBean(TaskTester.class).await(); + scheduledTask.cancel(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN") + .hasLowCardinalityKeyValue("code.function", "cancelled") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".CancelledReactiveTaskBean") + .hasLowCardinalityKeyValue("exception", "none"); + } + + @Test + void shouldHaveCurrentObservationInScope() throws Exception { + registerScheduledBean(CurrentObservationBean.class); + runScheduledTaskAndAwait(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") + .hasLowCardinalityKeyValue("code.function", "hasCurrentObservation") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".CurrentObservationBean") + .hasLowCardinalityKeyValue("exception", "none"); + } + + @Test + void shouldHaveCurrentObservationInReactiveScope() throws Exception { + registerScheduledBean(CurrentObservationReactiveBean.class); + runScheduledTaskAndAwait(); + assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") + .hasLowCardinalityKeyValue("code.function", "hasCurrentObservation") + .hasLowCardinalityKeyValue("code.namespace", getClass().getCanonicalName() + ".CurrentObservationReactiveBean") + .hasLowCardinalityKeyValue("exception", "none"); + } + + + private void registerScheduledBean(Class beanClass) { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(beanClass); + targetDefinition.getPropertyValues().add("observationRegistry", this.observationRegistry); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.registerBean("schedulingConfigurer", SchedulingConfigurer.class, () -> taskRegistrar -> taskRegistrar.setObservationRegistry(observationRegistry)); + context.refresh(); + } + + private ScheduledTask getScheduledTask() { + ScheduledTaskHolder taskHolder = context.getBean("postProcessor", ScheduledTaskHolder.class); + return taskHolder.getScheduledTasks().iterator().next(); + } + + private void runScheduledTaskAndAwait() throws InterruptedException { + ScheduledTask scheduledTask = getScheduledTask(); + try { + scheduledTask.getTask().getRunnable().run(); + } + catch (Throwable exc) { + // ignore exceptions thrown by test tasks + } + context.getBean(TaskTester.class).await(); + } + + private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatTaskObservation() { + return TestObservationRegistryAssert.assertThat(this.observationRegistry) + .hasObservationWithNameEqualTo("tasks.scheduled.execution").that(); + } + + + abstract static class TaskTester { + + ObservationRegistry observationRegistry; + + CountDownLatch latch = new CountDownLatch(1); + + public void setObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + void await() throws InterruptedException { + this.latch.await(3, TimeUnit.SECONDS); + } + } + + + static class FixedDelayBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + void fixedDelay() { + this.latch.countDown(); + } + } + + + static class FixedDelayErrorBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + void error() { + this.latch.countDown(); + throw new IllegalStateException("test error"); + } + } + + + static class FixedDelayReactiveBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + Mono fixedDelay() { + return Mono.empty().doOnTerminate(() -> this.latch.countDown()); + } + } + + + static class FixedDelayReactiveErrorBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + Mono error() { + return Mono.error(new IllegalStateException("test error")) + .doOnTerminate(() -> this.latch.countDown()); + } + } + + + static class CancelledTaskBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + void cancelled() { + this.latch.countDown(); + try { + Thread.sleep(5000); + } + catch (InterruptedException exc) { + // ignore cancelled task + } + } + } + + + static class CancelledReactiveTaskBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + Flux cancelled() { + return Flux.interval(Duration.ZERO, Duration.ofSeconds(1)) + .doOnNext(el -> this.latch.countDown()); + } + } + + + static class CurrentObservationBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + void hasCurrentObservation() { + Observation observation = this.observationRegistry.getCurrentObservation(); + assertThat(observation).isNotNull(); + assertThat(observation.getContext()).isInstanceOf(ScheduledTaskObservationContext.class); + this.latch.countDown(); + } + } + + + static class CurrentObservationReactiveBean extends TaskTester { + + @Scheduled(fixedDelay = 10_000, initialDelay = 5_000) + Mono hasCurrentObservation() { + return Mono.just("test") + .tap(() -> new DefaultSignalListener<>() { + @Override + public void doFirst() { + Observation observation = observationRegistry.getCurrentObservation(); + assertThat(observation).isNotNull(); + assertThat(observation.getContext()).isInstanceOf(ScheduledTaskObservationContext.class); + } + }) + .doOnTerminate(() -> this.latch.countDown()); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java index 417de9d8255c..179a2cbfa332 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; @@ -57,6 +58,7 @@ import org.springframework.scheduling.TriggerContext; import org.springframework.scheduling.config.CronTask; import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.OneTimeTask; import org.springframework.scheduling.config.ScheduledTaskHolder; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.support.CronTrigger; @@ -266,6 +268,51 @@ private void severalFixedRates(StaticApplicationContext context, assertThat(task2.getIntervalDuration()).isEqualTo(Duration.ofMillis(4_000L)); } + @Test + void oneTimeTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(OneTimeTaskBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks()).hasSize(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List oneTimeTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("oneTimeTasks"); + assertThat(oneTimeTasks).hasSize(1); + OneTimeTask task = oneTimeTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("oneTimeTask"); + assertThat(task.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(2_000L)); + } + + @Test + void oneTimeTaskOnNonRegisteredBean() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks()).hasSize(0); + + Object target = context.getAutowireCapableBeanFactory().createBean(OneTimeTaskBean.class); + assertThat(postProcessor.getScheduledTasks()).hasSize(1); + @SuppressWarnings("unchecked") + Set manualTasks = (Set) + new DirectFieldAccessor(postProcessor).getPropertyValue("manualCancellationOnContextClose"); + assertThat(manualTasks).hasSize(1); + assertThat(manualTasks).contains(target); + } + @Test void cronTask() { BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); @@ -324,9 +371,6 @@ void cronTaskWithZone() { assertThat(condition).isTrue(); CronTrigger cronTrigger = (CronTrigger) trigger; ZonedDateTime dateTime = ZonedDateTime.of(2013, 4, 15, 4, 0, 0, 0, ZoneId.of("GMT+10")); -// Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+10")); -// cal.clear(); -// cal.set(2013, 3, 15, 4, 0); // 15-04-2013 4:00 GMT+10; Instant lastScheduledExecution = dateTime.toInstant(); Instant lastActualExecution = dateTime.toInstant(); dateTime = dateTime.plusMinutes(30); @@ -514,7 +558,7 @@ void propertyPlaceholderWithInactiveCron() { context.refresh(); ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); - assertThat(postProcessor.getScheduledTasks().isEmpty()).isTrue(); + assertThat(postProcessor.getScheduledTasks()).isEmpty(); } @ParameterizedTest @@ -848,6 +892,14 @@ static class FixedRatesDefaultBean implements FixedRatesDefaultMethod { } + static class OneTimeTaskBean { + + @Scheduled(initialDelay = 2_000) + private void oneTimeTask() { + } + } + + @Validated static class CronTestBean { @@ -990,6 +1042,7 @@ void fixedDelay() { } } + static class PropertyPlaceholderWithFixedDelayInSeconds { @Scheduled(fixedDelayString = "${fixedDelay}", initialDelayString = "${initialDelay}", timeUnit = TimeUnit.SECONDS) @@ -1005,6 +1058,7 @@ void fixedRate() { } } + static class PropertyPlaceholderWithFixedRateInSeconds { @Scheduled(fixedRateString = "${fixedRate}", initialDelayString = "${initialDelay}", timeUnit = TimeUnit.SECONDS) @@ -1035,9 +1089,11 @@ void y() { } } + @Retention(RetentionPolicy.RUNTIME) @ConvertWith(NameToClass.Converter.class) private @interface NameToClass { + class Converter implements ArgumentConverter { @Override public Class convert(Object beanClassName, ParameterContext context) throws ArgumentConversionException { diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupportTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupportTests.java new file mode 100644 index 000000000000..3bdf797d8bab --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupportTests.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.annotation; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +import io.micrometer.observation.ObservationRegistry; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupport.createSubscriptionRunnable; +import static org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupport.getPublisherFor; +import static org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupport.isReactive; + +/** + * @author Simon Baslé + * @since 6.1 + */ +class ScheduledAnnotationReactiveSupportTests { + + @Test + void ensureReactor() { + assertThat(ScheduledAnnotationReactiveSupport.reactorPresent).isTrue(); + } + + @ParameterizedTest + // Note: monoWithParams can't be found by this test. + @ValueSource(strings = { "mono", "flux", "monoString", "fluxString", "publisherMono", + "publisherString", "monoThrows", "flowable", "completable" }) + void checkIsReactive(String method) { + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, method); + assertThat(isReactive(m)).as(m.getName()).isTrue(); + } + + @Test + void checkNotReactive() { + Method string = ReflectionUtils.findMethod(ReactiveMethods.class, "oops"); + + assertThat(isReactive(string)).as("String-returning").isFalse(); + } + + @Test + void rejectReactiveAdaptableButNotDeferred() { + Method future = ReflectionUtils.findMethod(ReactiveMethods.class, "future"); + + assertThatIllegalArgumentException().isThrownBy(() -> isReactive(future)) + .withMessage("Reactive methods may only be annotated with @Scheduled if the return type supports deferred execution"); + } + + @Test + void isReactiveRejectsWithParams() { + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "monoWithParam", String.class); + + // isReactive rejects with context + assertThatIllegalArgumentException().isThrownBy(() -> isReactive(m)) + .withMessage("Reactive methods may only be annotated with @Scheduled if declared without arguments") + .withNoCause(); + } + + @Test + void rejectCantProducePublisher() { + ReactiveMethods target = new ReactiveMethods(); + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "monoThrows"); + + // static helper method + assertThatIllegalArgumentException().isThrownBy(() -> getPublisherFor(m, target)) + .withMessage("Cannot obtain a Publisher-convertible value from the @Scheduled reactive method") + .withCause(new IllegalStateException("expected")); + } + + @Test + void rejectCantAccessMethod() { + ReactiveMethods target = new ReactiveMethods(); + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "monoThrowsIllegalAccess"); + + // static helper method + assertThatIllegalArgumentException().isThrownBy(() -> getPublisherFor(m, target)) + .withMessage("Cannot obtain a Publisher-convertible value from the @Scheduled reactive method") + .withCause(new IllegalAccessException("expected")); + } + + @Test + void fixedDelayIsBlocking() { + ReactiveMethods target = new ReactiveMethods(); + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "mono"); + Scheduled fixedDelayString = AnnotationUtils.synthesizeAnnotation(Map.of("fixedDelayString", "123"), Scheduled.class, null); + Scheduled fixedDelayLong = AnnotationUtils.synthesizeAnnotation(Map.of("fixedDelay", 123L), Scheduled.class, null); + List tracker = new ArrayList<>(); + + assertThat(createSubscriptionRunnable(m, target, fixedDelayString, () -> ObservationRegistry.NOOP, tracker)) + .isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr -> + assertThat(sr.shouldBlock).as("fixedDelayString.shouldBlock").isTrue() + ); + + assertThat(createSubscriptionRunnable(m, target, fixedDelayLong, () -> ObservationRegistry.NOOP, tracker)) + .isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr -> + assertThat(sr.shouldBlock).as("fixedDelayLong.shouldBlock").isTrue() + ); + } + + @Test + void fixedRateIsNotBlocking() { + ReactiveMethods target = new ReactiveMethods(); + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "mono"); + Scheduled fixedRateString = AnnotationUtils.synthesizeAnnotation(Map.of("fixedRateString", "123"), Scheduled.class, null); + Scheduled fixedRateLong = AnnotationUtils.synthesizeAnnotation(Map.of("fixedRate", 123L), Scheduled.class, null); + List tracker = new ArrayList<>(); + + assertThat(createSubscriptionRunnable(m, target, fixedRateString, () -> ObservationRegistry.NOOP, tracker)) + .isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr -> + assertThat(sr.shouldBlock).as("fixedRateString.shouldBlock").isFalse() + ); + + assertThat(createSubscriptionRunnable(m, target, fixedRateLong, () -> ObservationRegistry.NOOP, tracker)) + .isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr -> + assertThat(sr.shouldBlock).as("fixedRateLong.shouldBlock").isFalse() + ); + } + + @Test + void cronIsNotBlocking() { + ReactiveMethods target = new ReactiveMethods(); + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "mono"); + Scheduled cron = AnnotationUtils.synthesizeAnnotation(Map.of("cron", "-"), Scheduled.class, null); + List tracker = new ArrayList<>(); + + assertThat(createSubscriptionRunnable(m, target, cron, () -> ObservationRegistry.NOOP, tracker)) + .isInstanceOfSatisfying(ScheduledAnnotationReactiveSupport.SubscribingRunnable.class, sr -> + assertThat(sr.shouldBlock).as("cron.shouldBlock").isFalse() + ); + } + + @Test + void hasCheckpointToString() { + ReactiveMethods target = new ReactiveMethods(); + Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "mono"); + Publisher p = getPublisherFor(m, target); + + assertThat(p.getClass().getName()) + .as("checkpoint class") + .isEqualTo("reactor.core.publisher.FluxOnAssembly"); + + assertThat(p).hasToString("checkpoint(\"@Scheduled 'mono()' in 'org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupportTests$ReactiveMethods'\")"); + } + + + static class ReactiveMethods { + + public String oops() { + return "oops"; + } + + public Mono mono() { + return Mono.empty(); + } + + public Flux flux() { + return Flux.empty(); + } + + public Mono monoString() { + return Mono.just("example"); + } + + public Flux fluxString() { + return Flux.just("example"); + } + + public Publisher publisherMono() { + return Mono.empty(); + } + + public Publisher publisherString() { + return fluxString(); + } + + public CompletableFuture future() { + return CompletableFuture.completedFuture("example"); + } + + public Mono monoWithParam(String param) { + return Mono.just(param).then(); + } + + public Mono monoThrows() { + throw new IllegalStateException("expected"); + } + + public Mono monoThrowsIllegalAccess() throws IllegalAccessException { + // simulate a reflection issue + throw new IllegalAccessException("expected"); + } + + public Flowable flowable() { + return Flowable.empty(); + } + + public Completable completable() { + return Completable.complete(); + } + + AtomicInteger subscription = new AtomicInteger(); + + public Mono trackingMono() { + return Mono.empty().doOnSubscribe(s -> subscription.incrementAndGet()); + } + + public Mono monoError() { + return Mono.error(new IllegalStateException("expected")); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java index 5db612c1f8ba..5d817ee9e166 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,7 +89,7 @@ void executeFailingRunnable() { executor.execute(task); Awaitility.await() .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> task.exception.get() != null && task.exception.get().getMessage().equals( "TestTask failure for test 'executeFailingRunnable': expectedRunCount:<0>, actualRunCount:<1>")); @@ -133,7 +133,7 @@ void submitListenableRunnable() { future.addCallback(result -> outcome = result, ex -> outcome = ex); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(future::isDone); assertThat(outcome).isNull(); @@ -148,7 +148,7 @@ void submitCompletableRunnable() { future.whenComplete(this::storeOutcome); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(future::isDone); assertThat(outcome).isNull(); @@ -164,7 +164,7 @@ void submitFailingListenableRunnable() { Awaitility.await() .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> future.isDone() && outcome != null); assertThat(outcome.getClass()).isSameAs(RuntimeException.class); @@ -178,7 +178,7 @@ void submitFailingCompletableRunnable() { Awaitility.await() .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> future.isDone() && outcome != null); assertThat(outcome.getClass()).isSameAs(CompletionException.class); @@ -198,7 +198,7 @@ void submitListenableRunnableWithGetAfterShutdown() throws Exception { // ignore } Awaitility.await() - .atMost(4, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .untilAsserted(() -> assertThatExceptionOfType(CancellationException.class) .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); @@ -217,7 +217,7 @@ void submitCompletableRunnableWithGetAfterShutdown() throws Exception { // ignore } Awaitility.await() - .atMost(4, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .untilAsserted(() -> assertThatExceptionOfType(TimeoutException.class) .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); @@ -253,7 +253,7 @@ void submitCallableWithGetAfterShutdown() throws Exception { // ignore } Awaitility.await() - .atMost(4, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .untilAsserted(() -> assertThatExceptionOfType(CancellationException.class) .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); @@ -268,7 +268,7 @@ void submitListenableCallable() { future.addCallback(result -> outcome = result, ex -> outcome = ex); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> future.isDone() && outcome != null); assertThat(outcome.toString().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix); @@ -284,7 +284,7 @@ void submitFailingListenableCallable() { // Assert Awaitility.await() .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> future.isDone() && outcome != null); assertThat(outcome.getClass()).isSameAs(RuntimeException.class); @@ -310,7 +310,7 @@ void submitCompletableCallable() { future.whenComplete(this::storeOutcome); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> future.isDone() && outcome != null); assertThat(outcome.toString().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix); @@ -325,7 +325,7 @@ void submitFailingCompletableCallable() { // Assert Awaitility.await() .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .until(() -> future.isDone() && outcome != null); assertThat(outcome.getClass()).isSameAs(CompletionException.class); @@ -427,7 +427,7 @@ static class TestCallable implements Callable { } @Override - public String call() throws Exception { + public String call() { try { Thread.sleep(10); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java index 2ffe10450e32..7b011c9eb675 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package org.springframework.scheduling.concurrent; import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -52,8 +52,8 @@ protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecuto @AfterEach void shutdownExecutor() { for (Runnable task : concurrentExecutor.shutdownNow()) { - if (task instanceof RunnableFuture) { - ((RunnableFuture) task).cancel(true); + if (task instanceof Future) { + ((Future) task).cancel(true); } } } @@ -61,6 +61,7 @@ void shutdownExecutor() { @Test void zeroArgCtorResultsInDefaultTaskExecutorBeingUsed() { + @SuppressWarnings("deprecation") ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(); assertThatCode(() -> executor.execute(new NoOpRunnable())).doesNotThrowAnyException(); } @@ -68,11 +69,12 @@ void zeroArgCtorResultsInDefaultTaskExecutorBeingUsed() { @Test void passingNullExecutorToCtorResultsInDefaultTaskExecutorBeingUsed() { ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(null); - assertThatCode(() -> executor.execute(new NoOpRunnable())).doesNotThrowAnyException(); + assertThatCode(() -> executor.execute(new NoOpRunnable())).hasMessage("Executor not configured"); } @Test void earlySetConcurrentExecutorCallRespectsConfiguredTaskDecorator() { + @SuppressWarnings("deprecation") ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(); executor.setConcurrentExecutor(new DecoratedExecutor()); executor.setTaskDecorator(new RunnableDecorator()); @@ -81,6 +83,7 @@ void earlySetConcurrentExecutorCallRespectsConfiguredTaskDecorator() { @Test void lateSetConcurrentExecutorCallRespectsConfiguredTaskDecorator() { + @SuppressWarnings("deprecation") ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(); executor.setTaskDecorator(new RunnableDecorator()); executor.setConcurrentExecutor(new DecoratedExecutor()); diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/CronTriggerExecutionTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/CronTriggerExecutionTests.java new file mode 100644 index 000000000000..7dd614147bf9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/CronTriggerExecutionTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.concurrent; + +import java.time.Clock; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scheduling.support.CronTrigger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Juergen Hoeller + * @since 6.1.3 + */ +@EnabledForTestGroups(LONG_RUNNING) +class CronTriggerExecutionTests { + + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + + AtomicInteger count = new AtomicInteger(); + + Runnable quick = count::incrementAndGet; + + Runnable slow = () -> { + count.incrementAndGet(); + try { + Thread.sleep(1000); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + + @BeforeEach + void initialize() { + scheduler.initialize(); + } + + @AfterEach + void shutdown() { + scheduler.shutdown(); + } + + + @Test + void forLenientExecutionQuick() throws Exception { + scheduler.schedule(quick, CronTrigger.forLenientExecution("*/1 * * * * *")); + Thread.sleep(2000); + assertThat(count.get()).isEqualTo(2); + } + + @Test + void forLenientExecutionSlow() throws Exception { + scheduler.schedule(slow, CronTrigger.forLenientExecution("*/1 * * * * *")); + Thread.sleep(2000); + assertThat(count.get()).isEqualTo(1); + } + + @Test + void forFixedExecutionQuick() throws Exception { + scheduler.schedule(quick, CronTrigger.forFixedExecution("*/1 * * * * *")); + Thread.sleep(2000); + assertThat(count.get()).isEqualTo(2); + } + + @Test + void forFixedExecutionSlow() throws Exception { + scheduler.schedule(slow, CronTrigger.forFixedExecution("*/1 * * * * *")); + Thread.sleep(2000); + assertThat(count.get()).isEqualTo(2); + } + + @Test + void resumeLenientExecution() throws Exception { + scheduler.schedule(quick, CronTrigger.resumeLenientExecution("*/1 * * * * *", + Clock.systemDefaultZone().instant().minusSeconds(2))); + Thread.sleep(1000); + assertThat(count.get()).isEqualTo(2); + } + + @Test + void resumeFixedExecution() throws Exception { + scheduler.schedule(quick, CronTrigger.resumeFixedExecution("*/1 * * * * *", + Clock.systemDefaultZone().instant().minusSeconds(2))); + Thread.sleep(1000); + assertThat(count.get()).isEqualTo(3); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/DefaultManagedTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/DefaultManagedTaskSchedulerTests.java new file mode 100644 index 000000000000..9f161418874a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/DefaultManagedTaskSchedulerTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.concurrent; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.Test; + +import org.springframework.scheduling.Trigger; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultManagedTaskScheduler}. + * + * @author Stephane Nicoll + */ +class DefaultManagedTaskSchedulerTests { + + private final Runnable NO_OP = () -> {}; + + @Test + void scheduleWithTriggerAndNoScheduledExecutorProvidesDedicatedException() { + DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler(); + assertNoExecutorException(() -> scheduler.schedule(NO_OP, mock(Trigger.class))); + } + + @Test + void scheduleWithInstantAndNoScheduledExecutorProvidesDedicatedException() { + DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler(); + assertNoExecutorException(() -> scheduler.schedule(NO_OP, Instant.now())); + } + + @Test + void scheduleAtFixedRateWithStartTimeAndDurationAndNoScheduledExecutorProvidesDedicatedException() { + DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler(); + assertNoExecutorException(() -> scheduler.scheduleAtFixedRate( + NO_OP, Instant.now(), Duration.of(1, ChronoUnit.MINUTES))); + } + + @Test + void scheduleAtFixedRateWithDurationAndNoScheduledExecutorProvidesDedicatedException() { + DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler(); + assertNoExecutorException(() -> scheduler.scheduleAtFixedRate( + NO_OP, Duration.of(1, ChronoUnit.MINUTES))); + } + + @Test + void scheduleWithFixedDelayWithStartTimeAndDurationAndNoScheduledExecutorProvidesDedicatedException() { + DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler(); + assertNoExecutorException(() -> scheduler.scheduleWithFixedDelay( + NO_OP, Instant.now(), Duration.of(1, ChronoUnit.MINUTES))); + } + + @Test + void scheduleWithFixedDelayWithDurationAndNoScheduledExecutorProvidesDedicatedException() { + DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler(); + assertNoExecutorException(() -> scheduler.scheduleWithFixedDelay( + NO_OP, Duration.of(1, ChronoUnit.MINUTES))); + } + + private void assertNoExecutorException(ThrowingCallable callable) { + assertThatThrownBy(callable) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No ScheduledExecutor is configured"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java index 50cbfe834073..057d50df537e 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -219,7 +219,7 @@ void objectTypeReportsCorrectType() { private static void pauseToLetTaskStart(int seconds) { try { - Thread.sleep(seconds * 1000); + Thread.sleep(seconds * 1000L); } catch (InterruptedException ignored) { } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java index bbbff64f7119..2be063371f17 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type; /** - * Unit tests for {@link ThreadPoolTaskExecutor}. + * Tests for {@link ThreadPoolTaskExecutor}. * * @author Juergen Hoeller * @author Sam Brannen diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java index 0bff61ceea3c..64a1144f7fb3 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,7 +101,7 @@ void scheduleOneTimeTask() throws Exception { @Test @SuppressWarnings("deprecation") - void scheduleOneTimeFailingTaskWithoutErrorHandler() throws Exception { + void scheduleOneTimeFailingTaskWithoutErrorHandler() { TestTask task = new TestTask(this.testName, 0); Future future = scheduler.schedule(task, new Date()); assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS)); @@ -147,7 +147,7 @@ private void await(CountDownLatch latch) { catch (InterruptedException ex) { throw new IllegalStateException(ex); } - assertThat(latch.getCount()).as("latch did not count down,").isEqualTo(0); + assertThat(latch.getCount()).as("latch did not count down").isEqualTo(0); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParserTests.java index cd7e0d8037db..63924437290c 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,44 +31,44 @@ * @author Mark Fisher * @author Stephane Nicoll */ -public class AnnotationDrivenBeanDefinitionParserTests { +class AnnotationDrivenBeanDefinitionParserTests { private ConfigurableApplicationContext context = new ClassPathXmlApplicationContext( "annotationDrivenContext.xml", AnnotationDrivenBeanDefinitionParserTests.class); @AfterEach - public void closeApplicationContext() { + void closeApplicationContext() { context.close(); } @Test - public void asyncPostProcessorRegistered() { + void asyncPostProcessorRegistered() { assertThat(context.containsBean(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); } @Test - public void scheduledPostProcessorRegistered() { + void scheduledPostProcessorRegistered() { assertThat(context.containsBean(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); } @Test - public void asyncPostProcessorExecutorReference() { + void asyncPostProcessorExecutorReference() { Object executor = context.getBean("testExecutor"); Object postProcessor = context.getBean(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME); assertThat(((Supplier) new DirectFieldAccessor(postProcessor).getPropertyValue("executor")).get()).isSameAs(executor); } @Test - public void scheduledPostProcessorSchedulerReference() { + void scheduledPostProcessorSchedulerReference() { Object scheduler = context.getBean("testScheduler"); Object postProcessor = context.getBean(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME); assertThat(new DirectFieldAccessor(postProcessor).getPropertyValue("scheduler")).isSameAs(scheduler); } @Test - public void asyncPostProcessorExceptionHandlerReference() { + void asyncPostProcessorExceptionHandlerReference() { Object exceptionHandler = context.getBean("testExceptionHandler"); Object postProcessor = context.getBean(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME); assertThat(((Supplier) new DirectFieldAccessor(postProcessor).getPropertyValue("exceptionHandler")).get()).isSameAs(exceptionHandler); diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParserTests.java index ef8f32846ef3..fe22251fe841 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParserTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,20 +37,20 @@ * @author Mark Fisher * @author Juergen Hoeller */ -public class ExecutorBeanDefinitionParserTests { +class ExecutorBeanDefinitionParserTests { private ApplicationContext context; @BeforeEach - public void setup() { + void setup() { this.context = new ClassPathXmlApplicationContext( "executorContext.xml", ExecutorBeanDefinitionParserTests.class); } @Test - public void defaultExecutor() throws Exception { + void defaultExecutor() throws Exception { ThreadPoolTaskExecutor executor = this.context.getBean("default", ThreadPoolTaskExecutor.class); assertThat(getCorePoolSize(executor)).isEqualTo(1); assertThat(getMaxPoolSize(executor)).isEqualTo(Integer.MAX_VALUE); @@ -64,20 +64,20 @@ public void defaultExecutor() throws Exception { } @Test - public void singleSize() { + void singleSize() { Object executor = this.context.getBean("singleSize"); assertThat(getCorePoolSize(executor)).isEqualTo(42); assertThat(getMaxPoolSize(executor)).isEqualTo(42); } @Test - public void invalidPoolSize() { + void invalidPoolSize() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> this.context.getBean("invalidPoolSize")); } @Test - public void rangeWithBoundedQueue() { + void rangeWithBoundedQueue() { Object executor = this.context.getBean("rangeWithBoundedQueue"); assertThat(getCorePoolSize(executor)).isEqualTo(7); assertThat(getMaxPoolSize(executor)).isEqualTo(42); @@ -85,7 +85,7 @@ public void rangeWithBoundedQueue() { } @Test - public void rangeWithUnboundedQueue() { + void rangeWithUnboundedQueue() { Object executor = this.context.getBean("rangeWithUnboundedQueue"); assertThat(getCorePoolSize(executor)).isEqualTo(9); assertThat(getMaxPoolSize(executor)).isEqualTo(9); @@ -95,7 +95,7 @@ public void rangeWithUnboundedQueue() { } @Test - public void propertyPlaceholderWithSingleSize() { + void propertyPlaceholderWithSingleSize() { Object executor = this.context.getBean("propertyPlaceholderWithSingleSize"); assertThat(getCorePoolSize(executor)).isEqualTo(123); assertThat(getMaxPoolSize(executor)).isEqualTo(123); @@ -105,7 +105,7 @@ public void propertyPlaceholderWithSingleSize() { } @Test - public void propertyPlaceholderWithRange() { + void propertyPlaceholderWithRange() { Object executor = this.context.getBean("propertyPlaceholderWithRange"); assertThat(getCorePoolSize(executor)).isEqualTo(5); assertThat(getMaxPoolSize(executor)).isEqualTo(25); @@ -114,7 +114,7 @@ public void propertyPlaceholderWithRange() { } @Test - public void propertyPlaceholderWithRangeAndCoreThreadTimeout() { + void propertyPlaceholderWithRangeAndCoreThreadTimeout() { Object executor = this.context.getBean("propertyPlaceholderWithRangeAndCoreThreadTimeout"); assertThat(getCorePoolSize(executor)).isEqualTo(99); assertThat(getMaxPoolSize(executor)).isEqualTo(99); @@ -122,19 +122,19 @@ public void propertyPlaceholderWithRangeAndCoreThreadTimeout() { } @Test - public void propertyPlaceholderWithInvalidPoolSize() { + void propertyPlaceholderWithInvalidPoolSize() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> this.context.getBean("propertyPlaceholderWithInvalidPoolSize")); } @Test - public void threadNamePrefix() { + void threadNamePrefix() { CustomizableThreadCreator executor = this.context.getBean("default", CustomizableThreadCreator.class); assertThat(executor.getThreadNamePrefix()).isEqualTo("default-"); } @Test - public void typeCheck() { + void typeCheck() { assertThat(this.context.isTypeMatch("default", Executor.class)).isTrue(); assertThat(this.context.isTypeMatch("default", TaskExecutor.class)).isTrue(); assertThat(this.context.isTypeMatch("default", ThreadPoolTaskExecutor.class)).isTrue(); diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskRegistrarTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskRegistrarTests.java index 482a3197750e..e8a663179cda 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskRegistrarTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for {@link ScheduledTaskRegistrar}. + * Tests for {@link ScheduledTaskRegistrar}. * * @author Tobias Montagna-Hay * @author Juergen Hoeller diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java index dde3a4faa6d4..c6ab6d340f9f 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ public class ScheduledTasksBeanDefinitionParserTests { @BeforeEach - public void setup() { + void setup() { this.context = new ClassPathXmlApplicationContext( "scheduledTasksContext.xml", ScheduledTasksBeanDefinitionParserTests.class); this.registrar = this.context.getBeansOfType( @@ -57,14 +57,14 @@ public void setup() { } @Test - public void checkScheduler() { + void checkScheduler() { Object schedulerBean = this.context.getBean("testScheduler"); Object schedulerRef = new DirectFieldAccessor(this.registrar).getPropertyValue("taskScheduler"); assertThat(schedulerRef).isEqualTo(schedulerBean); } @Test - public void checkTarget() { + void checkTarget() { List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("fixedRateTasks"); Runnable runnable = tasks.get(0).getRunnable(); @@ -76,7 +76,7 @@ public void checkTarget() { } @Test - public void fixedRateTasks() { + void fixedRateTasks() { List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("fixedRateTasks"); assertThat(tasks).hasSize(3); @@ -87,7 +87,7 @@ public void fixedRateTasks() { } @Test - public void fixedDelayTasks() { + void fixedDelayTasks() { List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("fixedDelayTasks"); assertThat(tasks).hasSize(2); @@ -97,7 +97,7 @@ public void fixedDelayTasks() { } @Test - public void cronTasks() { + void cronTasks() { List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("cronTasks"); assertThat(tasks).hasSize(1); @@ -105,7 +105,7 @@ public void cronTasks() { } @Test - public void triggerTasks() { + void triggerTasks() { List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("triggerTasks"); assertThat(tasks).hasSize(1); diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParserTests.java index 6bf38e40fb8c..6c7dfcb8611e 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParserTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,33 +29,33 @@ /** * @author Mark Fisher */ -public class SchedulerBeanDefinitionParserTests { +class SchedulerBeanDefinitionParserTests { private ApplicationContext context; @BeforeEach - public void setup() { + void setup() { this.context = new ClassPathXmlApplicationContext( "schedulerContext.xml", SchedulerBeanDefinitionParserTests.class); } @Test - public void defaultScheduler() { + void defaultScheduler() { ThreadPoolTaskScheduler scheduler = (ThreadPoolTaskScheduler) this.context.getBean("defaultScheduler"); Integer size = (Integer) new DirectFieldAccessor(scheduler).getPropertyValue("poolSize"); assertThat(size).isEqualTo(1); } @Test - public void customScheduler() { + void customScheduler() { ThreadPoolTaskScheduler scheduler = (ThreadPoolTaskScheduler) this.context.getBean("customScheduler"); Integer size = (Integer) new DirectFieldAccessor(scheduler).getPropertyValue("poolSize"); assertThat(size).isEqualTo(42); } @Test - public void threadNamePrefix() { + void threadNamePrefix() { ThreadPoolTaskScheduler scheduler = (ThreadPoolTaskScheduler) this.context.getBean("customScheduler"); assertThat(scheduler.getThreadNamePrefix()).isEqualTo("customScheduler-"); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java index 961312a64ff8..b1fe23b829b3 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link BitsCronField}. + * Tests for {@link BitsCronField}. * * @author Arjen Poutsma * @author Sam Brannen @@ -35,27 +35,32 @@ class BitsCronFieldTests { @Test void parse() { assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59)); - assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)).has(setRange(8, 12)).has(clearRange(13,59)); - assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)).has(clear(58)).has(set(59)); + assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)) + .has(setRange(8, 12)).has(clearRange(13,59)); + assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)) + .has(clear(58)).has(set(59)); assertThat(BitsCronField.parseMinutes("30")).has(set(30)).has(clearRange(1, 29)).has(clearRange(31, 59)); assertThat(BitsCronField.parseHours("23")).has(set(23)).has(clearRange(0, 23)); - assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); + assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)) + .has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); assertThat(BitsCronField.parseDaysOfMonth("1")).has(set(1)).has(clearRange(2, 31)); assertThat(BitsCronField.parseMonth("1")).has(set(1)).has(clearRange(2, 12)); assertThat(BitsCronField.parseDaysOfWeek("0")).has(set(7, 7)).has(clearRange(0, 6)); - - assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)).has(clear(6)).has(set(7)); + assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)) + .has(clear(6)).has(set(7)); } @Test void parseLists() { - assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)).has(clearRange(31, 59)); - assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)).has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59)); + assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)) + .has(clearRange(31, 59)); + assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)) + .has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59)); assertThat(BitsCronField.parseHours("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 23)); assertThat(BitsCronField.parseDaysOfMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 31)); assertThat(BitsCronField.parseMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 12)); @@ -107,6 +112,7 @@ void names() { .has(clear(0)).has(setRange(1, 7)); } + private static Condition set(int... indices) { return new Condition<>(String.format("set bits %s", Arrays.toString(indices))) { @Override diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index 9f946b6cd16c..57372204cb7f 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ public boolean matches(Temporal value) { }; @Test - public void isValidExpression() { + void isValidExpression() { assertThat(CronExpression.isValidExpression(null)).isFalse(); assertThat(CronExpression.isValidExpression("")).isFalse(); assertThat(CronExpression.isValidExpression("*")).isFalse(); @@ -479,7 +479,7 @@ void monthSequence() { } @Test - public void fixedDays() { + void fixedDays() { CronExpression expression = CronExpression.parse("0 0 0 29 2 WED"); LocalDateTime last = LocalDateTime.of(2012, 2, 29, 1, 0); @@ -509,7 +509,7 @@ void friday13th() { } @Test - public void everyTenDays() { + void everyTenDays() { CronExpression cronExpression = CronExpression.parse("0 15 12 */10 1-8 5"); LocalDateTime last = LocalDateTime.parse("2021-04-30T12:14:59"); @@ -806,7 +806,7 @@ void quartzLastWeekdayOfMonth() { } @Test - public void quartzLastDayOfWeekOffset() { + void quartzLastDayOfWeekOffset() { // last Friday (5) of the month CronExpression expression = CronExpression.parse("0 0 0 * * 5L"); @@ -1295,7 +1295,7 @@ void quartzLastFridayOfTheMonthEveryHour() { } @Test - public void sundayToFriday() { + void sundayToFriday() { CronExpression expression = CronExpression.parse("0 0 0 ? * SUN-FRI"); LocalDateTime last = LocalDateTime.of(2021, 2, 25, 15, 0); @@ -1314,7 +1314,7 @@ public void sundayToFriday() { } @Test - public void daylightSaving() { + void daylightSaving() { CronExpression cronExpression = CronExpression.parse("0 0 9 * * *"); ZonedDateTime last = ZonedDateTime.parse("2021-03-27T09:00:00+01:00[Europe/Amsterdam]"); @@ -1355,9 +1355,9 @@ public void daylightSaving() { } @Test - public void various() { + void various() { CronExpression cronExpression = CronExpression.parse("3-57 13-28 17,18 1,15 3-12 6#1"); - LocalDateTime last = LocalDateTime.of(2022, 9, 15, 17, 44, 11); + LocalDateTime last = LocalDateTime.of(2022, 9, 15, 17, 44, 11); LocalDateTime expected = LocalDateTime.of(2022, 10, 1, 17, 13, 3); LocalDateTime actual = cronExpression.next(last); assertThat(actual).isNotNull(); diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronSequenceGeneratorTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronSequenceGeneratorTests.java deleted file mode 100644 index 73895c8619d0..000000000000 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronSequenceGeneratorTests.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.scheduling.support; - -import java.util.Date; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * @author Juergen Hoeller - * @author Ruslan Sibgatullin - */ -@SuppressWarnings({ "removal", "deprecation" }) -public class CronSequenceGeneratorTests { - - @Test - public void at50Seconds() { - assertThat(new CronSequenceGenerator("*/15 * 1-4 * * *").next(new Date(2012, 6, 1, 9, 53, 50))).isEqualTo(new Date(2012, 6, 2, 1, 0)); - } - - @Test - public void at0Seconds() { - assertThat(new CronSequenceGenerator("*/15 * 1-4 * * *").next(new Date(2012, 6, 1, 9, 53))).isEqualTo(new Date(2012, 6, 2, 1, 0)); - } - - @Test - public void at0Minutes() { - assertThat(new CronSequenceGenerator("0 */2 1-4 * * *").next(new Date(2012, 6, 1, 9, 0))).isEqualTo(new Date(2012, 6, 2, 1, 0)); - } - - @Test - public void with0Increment() { - assertThatIllegalArgumentException().isThrownBy(() -> - new CronSequenceGenerator("*/0 * * * * *").next(new Date(2012, 6, 1, 9, 0))); - } - - @Test - public void withNegativeIncrement() { - assertThatIllegalArgumentException().isThrownBy(() -> - new CronSequenceGenerator("*/-1 * * * * *").next(new Date(2012, 6, 1, 9, 0))); - } - - @Test - public void withInvertedMinuteRange() { - assertThatIllegalArgumentException().isThrownBy(() -> - new CronSequenceGenerator("* 6-5 * * * *").next(new Date(2012, 6, 1, 9, 0))); - } - - @Test - public void withInvertedHourRange() { - assertThatIllegalArgumentException().isThrownBy(() -> - new CronSequenceGenerator("* * 6-5 * * *").next(new Date(2012, 6, 1, 9, 0))); - } - - @Test - public void withSameMinuteRange() { - new CronSequenceGenerator("* 6-6 * * * *").next(new Date(2012, 6, 1, 9, 0)); - } - - @Test - public void withSameHourRange() { - new CronSequenceGenerator("* * 6-6 * * *").next(new Date(2012, 6, 1, 9, 0)); - } - - @Test - public void validExpression() { - assertThat(CronSequenceGenerator.isValidExpression("0 */2 1-4 * * *")).isTrue(); - } - - @Test - public void invalidExpressionWithLength() { - assertThat(CronSequenceGenerator.isValidExpression("0 */2 1-4 * * * *")).isFalse(); - } - - @Test - public void invalidExpressionWithSeconds() { - assertThat(CronSequenceGenerator.isValidExpression("100 */2 1-4 * * *")).isFalse(); - } - - @Test - public void invalidExpressionWithMonths() { - assertThat(CronSequenceGenerator.isValidExpression("0 */2 1-4 * INVALID *")).isFalse(); - } - - @Test - public void nullExpression() { - assertThat(CronSequenceGenerator.isValidExpression(null)).isFalse(); - } - -} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java index d1f58624c4c1..91c15e570893 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; /** - * Unit tests for {@link CronTrigger}. + * Tests for {@link CronTrigger}. * * @author Dave Syer * @author Mark Fisher @@ -848,6 +848,7 @@ void daylightSavingMissingHour(Date localDateTime, TimeZone timeZone) { assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime()); } + private static void roundup(Calendar calendar) { calendar.add(Calendar.SECOND, 1); calendar.set(Calendar.MILLISECOND, 0); @@ -861,9 +862,7 @@ private static void assertMatchesNextSecond(CronTrigger trigger, Calendar calend } private static TriggerContext getTriggerContext(Date lastCompletionTime) { - SimpleTriggerContext context = new SimpleTriggerContext(); - context.update(null, null, lastCompletionTime); - return context; + return new SimpleTriggerContext(null, null, lastCompletionTime); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConventionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConventionTests.java new file mode 100644 index 000000000000..408d0a33ab3b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConventionTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.support; + + +import java.lang.reflect.Method; + +import io.micrometer.common.KeyValue; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultScheduledTaskObservationConvention}. + */ +class DefaultScheduledTaskObservationConventionTests { + + private final Method taskMethod = ClassUtils.getMethod(BeanWithScheduledMethods.class, "process"); + + private final Method runMethod = ClassUtils.getMethod(Runnable.class, "run"); + + private final ScheduledTaskObservationConvention convention = new DefaultScheduledTaskObservationConvention(); + + + @Test + void observationShouldHaveDefaultName() { + assertThat(convention.getName()).isEqualTo("tasks.scheduled.execution"); + } + + @Test + void observationShouldHaveContextualName() { + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); + assertThat(convention.getContextualName(context)).isEqualTo("task beanWithScheduledMethods.process"); + } + + @Test + void observationShouldHaveContextualNameForProxiedClass() { + Object proxy = ProxyFactory.getProxy(new SingletonTargetSource(new BeanWithScheduledMethods())); + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(proxy, taskMethod); + assertThat(convention.getContextualName(context)).isEqualTo("task beanWithScheduledMethods.process"); + } + + @Test + void observationShouldHaveTargetType() { + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); + assertThat(convention.getLowCardinalityKeyValues(context)) + .contains(KeyValue.of("code.namespace", getClass().getCanonicalName() + ".BeanWithScheduledMethods")); + } + + @Test + void observationShouldHaveMethodName() { + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); + assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("code.function", "process")); + } + + @Test + void observationShouldHaveTargetTypeForAnonymousClass() { + Runnable runnable = () -> { }; + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(runnable, runMethod); + assertThat(convention.getLowCardinalityKeyValues(context)) + .contains(KeyValue.of("code.namespace", "ANONYMOUS")); + } + + @Test + void observationShouldHaveMethodNameForAnonymousClass() { + Runnable runnable = () -> { }; + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(runnable, runMethod); + assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("code.function", "run")); + } + + @Test + void observationShouldHaveSuccessfulOutcome() { + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); + context.setComplete(true); + assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "SUCCESS"), + KeyValue.of("exception", "none")); + } + + @Test + void observationShouldHaveErrorOutcome() { + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); + context.setError(new IllegalStateException("test error")); + assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "ERROR"), + KeyValue.of("exception", "IllegalStateException")); + } + + @Test + void observationShouldHaveUnknownOutcome() { + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); + assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "UNKNOWN"), + KeyValue.of("exception", "none")); + } + + + interface TaskProcessor { + + void process(); + } + + + static class BeanWithScheduledMethods implements TaskProcessor { + + @Override + public void process() { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java index d06d41775fbd..ad1f6222d367 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java @@ -185,25 +185,25 @@ void equalsVerification() { PeriodicTrigger trigger1 = new PeriodicTrigger(Duration.ofMillis(3000)); PeriodicTrigger trigger2 = new PeriodicTrigger(Duration.ofMillis(3000)); assertThat(trigger1.equals(new String("not a trigger"))).isFalse(); - assertThat(trigger1.equals(null)).isFalse(); + assertThat(trigger1).isNotEqualTo(null); assertThat(trigger1).isEqualTo(trigger1); assertThat(trigger2).isEqualTo(trigger2); assertThat(trigger2).isEqualTo(trigger1); trigger2.setInitialDelay(1234); - assertThat(trigger1.equals(trigger2)).isFalse(); - assertThat(trigger2.equals(trigger1)).isFalse(); + assertThat(trigger1).isNotEqualTo(trigger2); + assertThat(trigger2).isNotEqualTo(trigger1); trigger1.setInitialDelay(1234); assertThat(trigger2).isEqualTo(trigger1); trigger2.setFixedRate(true); - assertThat(trigger1.equals(trigger2)).isFalse(); - assertThat(trigger2.equals(trigger1)).isFalse(); + assertThat(trigger1).isNotEqualTo(trigger2); + assertThat(trigger2).isNotEqualTo(trigger1); trigger1.setFixedRate(true); assertThat(trigger2).isEqualTo(trigger1); PeriodicTrigger trigger3 = new PeriodicTrigger(Duration.ofSeconds(3)); trigger3.setInitialDelay(Duration.ofSeconds(7)); trigger3.setFixedRate(true); - assertThat(trigger1.equals(trigger3)).isFalse(); - assertThat(trigger3.equals(trigger1)).isFalse(); + assertThat(trigger1).isNotEqualTo(trigger3); + assertThat(trigger3).isNotEqualTo(trigger1); trigger1.setInitialDelay(Duration.ofMillis(7000)); assertThat(trigger3).isEqualTo(trigger1); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java index ec56f366b5b2..22910b9e2b77 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,10 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link QuartzCronField}. + * Tests for {@link QuartzCronField}. * * @author Arjen Poutsma + * @author Juergen Hoeller */ class QuartzCronFieldTests { @@ -71,6 +72,46 @@ void lastDayOfWeekOffset() { assertThat(field.nextOrSame(last)).isEqualTo(expected); } + @Test + void dayOfWeek_0(){ + // third Sunday (0) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("0#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 21); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_1(){ + // third Monday (1) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("1#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 15); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_2(){ + // third Tuesday (2) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("2#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 16); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_7() { + // third Sunday (7 as alternative to 0) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("7#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 21); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + @Test void invalidValues() { assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("")); diff --git a/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptEvaluatorTests.java b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptEvaluatorTests.java index dff1f6424b81..20e40c3da118 100644 --- a/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,24 +31,24 @@ /** * @author Juergen Hoeller */ -public class BshScriptEvaluatorTests { +class BshScriptEvaluatorTests { @Test - public void testBshScriptFromString() { + void testBshScriptFromString() { ScriptEvaluator evaluator = new BshScriptEvaluator(); Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2;")); assertThat(result).isEqualTo(6); } @Test - public void testBshScriptFromFile() { + void testBshScriptFromFile() { ScriptEvaluator evaluator = new BshScriptEvaluator(); Object result = evaluator.evaluate(new ResourceScriptSource(new ClassPathResource("simple.bsh", getClass()))); assertThat(result).isEqualTo(6); } @Test - public void testGroovyScriptWithArguments() { + void testGroovyScriptWithArguments() { ScriptEvaluator evaluator = new BshScriptEvaluator(); Map arguments = new HashMap<>(); arguments.put("a", 3); diff --git a/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptFactoryTests.java b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptFactoryTests.java index e265610c69d3..2f650bfa9eeb 100644 --- a/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptFactoryTests.java @@ -54,8 +54,8 @@ class BshScriptFactoryTests { void staticScript() { ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class)).contains("calculator")).isTrue(); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class))).contains("calculator"); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messenger"); Calculator calc = (Calculator) ctx.getBean("calculator"); Messenger messenger = (Messenger) ctx.getBean("messenger"); @@ -78,32 +78,32 @@ void staticScript() { String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Calculator.class).values().contains(calc)).isTrue(); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Calculator.class)).containsValue(calc); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); ctx.close(); } @Test void staticScriptWithNullReturnValue() { ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerWithConfig")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerWithConfig"); ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerWithConfig"); messenger.setMessage(null); assertThat(messenger.getMessage()).isNull(); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); ctx.close(); } @Test void staticScriptWithTwoInterfacesSpecified() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerWithConfigExtra")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerWithConfigExtra"); ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerWithConfigExtra"); messenger.setMessage(null); assertThat(messenger.getMessage()).isNull(); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); ctx.close(); assertThat(messenger.getMessage()).isNull(); @@ -112,12 +112,12 @@ void staticScriptWithTwoInterfacesSpecified() { @Test void staticWithScriptReturningInstance() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstance")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerInstance"); Messenger messenger = (Messenger) ctx.getBean("messengerInstance"); String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); ctx.close(); assertThat(messenger.getMessage()).isNull(); @@ -126,12 +126,12 @@ void staticWithScriptReturningInstance() { @Test void staticScriptImplementingInterface() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerImpl")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerImpl"); Messenger messenger = (Messenger) ctx.getBean("messengerImpl"); String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); ctx.close(); assertThat(messenger.getMessage()).isNull(); @@ -249,9 +249,9 @@ void resourceScriptFromTag() { TestBean testBean = (TestBean) ctx.getBean("testBean"); Collection beanNames = Arrays.asList(ctx.getBeanNamesForType(Messenger.class)); - assertThat(beanNames.contains("messenger")).isTrue(); - assertThat(beanNames.contains("messengerImpl")).isTrue(); - assertThat(beanNames.contains("messengerInstance")).isTrue(); + assertThat(beanNames).contains("messenger"); + assertThat(beanNames).contains("messengerImpl"); + assertThat(beanNames).contains("messengerInstance"); Messenger messenger = (Messenger) ctx.getBean("messenger"); assertThat(messenger.getMessage()).isEqualTo("Hello World!"); @@ -271,11 +271,11 @@ void resourceScriptFromTag() { assertThat(messengerByName.getTestBean()).isEqualTo(testBean); Collection beans = ctx.getBeansOfType(Messenger.class).values(); - assertThat(beans.contains(messenger)).isTrue(); - assertThat(beans.contains(messengerImpl)).isTrue(); - assertThat(beans.contains(messengerInstance)).isTrue(); - assertThat(beans.contains(messengerByType)).isTrue(); - assertThat(beans.contains(messengerByName)).isTrue(); + assertThat(beans).contains(messenger); + assertThat(beans).contains(messengerImpl); + assertThat(beans).contains(messengerInstance); + assertThat(beans).contains(messengerByType); + assertThat(beans).contains(messengerByName); ctx.close(); assertThat(messenger.getMessage()).isNull(); diff --git a/spring-context/src/test/java/org/springframework/scripting/config/ScriptingDefaultsTests.java b/spring-context/src/test/java/org/springframework/scripting/config/ScriptingDefaultsTests.java index 770758e0e75b..d4bf35ae5d98 100644 --- a/spring-context/src/test/java/org/springframework/scripting/config/ScriptingDefaultsTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/config/ScriptingDefaultsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public class ScriptingDefaultsTests { @Test - public void defaultRefreshCheckDelay() throws Exception { + void defaultRefreshCheckDelay() throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); Advised advised = (Advised) context.getBean("testBean"); AbstractRefreshableTargetSource targetSource = @@ -55,21 +55,21 @@ public void defaultRefreshCheckDelay() throws Exception { } @Test - public void defaultInitMethod() { + void defaultInitMethod() { ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); ITestBean testBean = (ITestBean) context.getBean("testBean"); assertThat(testBean.isInitialized()).isTrue(); } @Test - public void nameAsAlias() { + void nameAsAlias() { ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); ITestBean testBean = (ITestBean) context.getBean("/url"); assertThat(testBean.isInitialized()).isTrue(); } @Test - public void defaultDestroyMethod() { + void defaultDestroyMethod() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); ITestBean testBean = (ITestBean) context.getBean("nonRefreshableTestBean"); assertThat(testBean.isDestroyed()).isFalse(); @@ -78,7 +78,7 @@ public void defaultDestroyMethod() { } @Test - public void defaultAutowire() { + void defaultAutowire() { ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); ITestBean testBean = (ITestBean) context.getBean("testBean"); ITestBean otherBean = (ITestBean) context.getBean("otherBean"); @@ -86,7 +86,7 @@ public void defaultAutowire() { } @Test - public void defaultProxyTargetClass() { + void defaultProxyTargetClass() { ApplicationContext context = new ClassPathXmlApplicationContext(PROXY_CONFIG); Object testBean = context.getBean("testBean"); assertThat(AopUtils.isCglibProxy(testBean)).isTrue(); diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectTests.java index 6f1550f7c8ea..18bc00e70828 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ * @author Dave Syer * @author Sam Brannen */ -public class GroovyAspectTests { +class GroovyAspectTests { private final LogUserAdvice logAdvice = new LogUserAdvice(); @@ -41,7 +41,7 @@ public class GroovyAspectTests { @Test - public void manualGroovyBeanWithUnconditionalPointcut() throws Exception { + void manualGroovyBeanWithUnconditionalPointcut() throws Exception { TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( new ClassPathResource("GroovyServiceImpl.grv", getClass()))); @@ -49,7 +49,7 @@ public void manualGroovyBeanWithUnconditionalPointcut() throws Exception { } @Test - public void manualGroovyBeanWithStaticPointcut() throws Exception { + void manualGroovyBeanWithStaticPointcut() throws Exception { TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( new ClassPathResource("GroovyServiceImpl.grv", getClass()))); @@ -59,7 +59,7 @@ public void manualGroovyBeanWithStaticPointcut() throws Exception { } @Test - public void manualGroovyBeanWithDynamicPointcut() throws Exception { + void manualGroovyBeanWithDynamicPointcut() throws Exception { TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( new ClassPathResource("GroovyServiceImpl.grv", getClass()))); @@ -69,7 +69,7 @@ public void manualGroovyBeanWithDynamicPointcut() throws Exception { } @Test - public void manualGroovyBeanWithDynamicPointcutProxyTargetClass() throws Exception { + void manualGroovyBeanWithDynamicPointcutProxyTargetClass() throws Exception { TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( new ClassPathResource("GroovyServiceImpl.grv", getClass()))); @@ -78,14 +78,13 @@ public void manualGroovyBeanWithDynamicPointcutProxyTargetClass() throws Excepti testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, target, "GroovyServiceImpl", true); } - private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) - throws Exception { + private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) { testAdvice(advisor, logAdvice, target, message, false); } private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message, - boolean proxyTargetClass) throws Exception { + boolean proxyTargetClass) { logAdvice.reset(); diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyClassLoadingTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyClassLoadingTests.java index 5bf307fbc731..e7cc29ddf7c9 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyClassLoadingTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyClassLoadingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ /** * @author Mark Fisher */ -public class GroovyClassLoadingTests { +class GroovyClassLoadingTests { @Test @SuppressWarnings("resource") diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptEvaluatorTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptEvaluatorTests.java index 4b584f0b2401..f2acc4f88f37 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,24 +33,24 @@ /** * @author Juergen Hoeller */ -public class GroovyScriptEvaluatorTests { +class GroovyScriptEvaluatorTests { @Test - public void testGroovyScriptFromString() { + void testGroovyScriptFromString() { ScriptEvaluator evaluator = new GroovyScriptEvaluator(); Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2")); assertThat(result).isEqualTo(6); } @Test - public void testGroovyScriptFromFile() { + void testGroovyScriptFromFile() { ScriptEvaluator evaluator = new GroovyScriptEvaluator(); Object result = evaluator.evaluate(new ResourceScriptSource(new ClassPathResource("simple.groovy", getClass()))); assertThat(result).isEqualTo(6); } @Test - public void testGroovyScriptWithArguments() { + void testGroovyScriptWithArguments() { ScriptEvaluator evaluator = new GroovyScriptEvaluator(); Map arguments = new HashMap<>(); arguments.put("a", 3); @@ -60,17 +60,17 @@ public void testGroovyScriptWithArguments() { } @Test - public void testGroovyScriptWithCompilerConfiguration() { + void testGroovyScriptWithCompilerConfiguration() { GroovyScriptEvaluator evaluator = new GroovyScriptEvaluator(); MyBytecodeProcessor processor = new MyBytecodeProcessor(); evaluator.getCompilerConfiguration().setBytecodePostprocessor(processor); Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2")); assertThat(result).isEqualTo(6); - assertThat(processor.processed.contains("Script1")).isTrue(); + assertThat(processor.processed).contains("Script1"); } @Test - public void testGroovyScriptWithImportCustomizer() { + void testGroovyScriptWithImportCustomizer() { GroovyScriptEvaluator evaluator = new GroovyScriptEvaluator(); ImportCustomizer importCustomizer = new ImportCustomizer(); importCustomizer.addStarImports("org.springframework.util"); @@ -80,7 +80,7 @@ public void testGroovyScriptWithImportCustomizer() { } @Test - public void testGroovyScriptFromStringUsingJsr223() { + void testGroovyScriptFromStringUsingJsr223() { StandardScriptEvaluator evaluator = new StandardScriptEvaluator(); evaluator.setLanguage("Groovy"); Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2")); @@ -88,14 +88,14 @@ public void testGroovyScriptFromStringUsingJsr223() { } @Test - public void testGroovyScriptFromFileUsingJsr223() { + void testGroovyScriptFromFileUsingJsr223() { ScriptEvaluator evaluator = new StandardScriptEvaluator(); Object result = evaluator.evaluate(new ResourceScriptSource(new ClassPathResource("simple.groovy", getClass()))); assertThat(result).isEqualTo(6); } @Test - public void testGroovyScriptWithArgumentsUsingJsr223() { + void testGroovyScriptWithArgumentsUsingJsr223() { StandardScriptEvaluator evaluator = new StandardScriptEvaluator(); evaluator.setLanguage("Groovy"); Map arguments = new HashMap<>(); diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java index ffe5a19f2a0b..812f4c442338 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,11 +66,11 @@ public class GroovyScriptFactoryTests { @Test - public void testStaticScript() throws Exception { + void testStaticScript() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class)).contains("calculator")).isTrue(); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class))).contains("calculator"); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messenger"); Calculator calc = (Calculator) ctx.getBean("calculator"); Messenger messenger = (Messenger) ctx.getBean("messenger"); @@ -94,16 +94,16 @@ public void testStaticScript() throws Exception { String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Calculator.class).values().contains(calc)).isTrue(); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Calculator.class)).containsValue(calc); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); } @Test - public void testStaticScriptUsingJsr223() throws Exception { + void testStaticScriptUsingJsr223() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class)).contains("calculator")).isTrue(); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class))).contains("calculator"); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messenger"); Calculator calc = (Calculator) ctx.getBean("calculator"); Messenger messenger = (Messenger) ctx.getBean("messenger"); @@ -127,12 +127,12 @@ public void testStaticScriptUsingJsr223() throws Exception { String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Calculator.class).values().contains(calc)).isTrue(); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Calculator.class)).containsValue(calc); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); } @Test - public void testStaticPrototypeScript() throws Exception { + void testStaticPrototypeScript() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); @@ -152,7 +152,7 @@ public void testStaticPrototypeScript() throws Exception { } @Test - public void testStaticPrototypeScriptUsingJsr223() throws Exception { + void testStaticPrototypeScriptUsingJsr223() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); @@ -172,9 +172,9 @@ public void testStaticPrototypeScriptUsingJsr223() throws Exception { } @Test - public void testStaticScriptWithInstance() throws Exception { + void testStaticScriptWithInstance() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstance")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerInstance"); Messenger messenger = (Messenger) ctx.getBean("messengerInstance"); assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); @@ -183,13 +183,13 @@ public void testStaticScriptWithInstance() throws Exception { String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); } @Test - public void testStaticScriptWithInstanceUsingJsr223() throws Exception { + void testStaticScriptWithInstanceUsingJsr223() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstance")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerInstance"); Messenger messenger = (Messenger) ctx.getBean("messengerInstance"); assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); @@ -198,13 +198,13 @@ public void testStaticScriptWithInstanceUsingJsr223() throws Exception { String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); } @Test - public void testStaticScriptWithInlineDefinedInstance() throws Exception { + void testStaticScriptWithInlineDefinedInstance() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstanceInline")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerInstanceInline"); Messenger messenger = (Messenger) ctx.getBean("messengerInstanceInline"); assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); @@ -213,13 +213,13 @@ public void testStaticScriptWithInlineDefinedInstance() throws Exception { String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); } @Test - public void testStaticScriptWithInlineDefinedInstanceUsingJsr223() throws Exception { + void testStaticScriptWithInlineDefinedInstanceUsingJsr223() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstanceInline")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerInstanceInline"); Messenger messenger = (Messenger) ctx.getBean("messengerInstanceInline"); assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); @@ -228,11 +228,11 @@ public void testStaticScriptWithInlineDefinedInstanceUsingJsr223() throws Except String desiredMessage = "Hello World!"; assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); } @Test - public void testNonStaticScript() throws Exception { + void testNonStaticScript() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyRefreshableContext.xml", getClass()); Messenger messenger = (Messenger) ctx.getBean("messenger"); @@ -251,7 +251,7 @@ public void testNonStaticScript() throws Exception { } @Test - public void testNonStaticPrototypeScript() throws Exception { + void testNonStaticPrototypeScript() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyRefreshableContext.xml", getClass()); ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); @@ -276,14 +276,14 @@ public void testNonStaticPrototypeScript() throws Exception { } @Test - public void testScriptCompilationException() throws Exception { + void testScriptCompilationException() { assertThatExceptionOfType(NestedRuntimeException.class).isThrownBy(() -> new ClassPathXmlApplicationContext("org/springframework/scripting/groovy/groovyBrokenContext.xml")) .matches(ex -> ex.contains(ScriptCompilationException.class)); } @Test - public void testScriptedClassThatDoesNotHaveANoArgCtor() throws Exception { + void testScriptedClassThatDoesNotHaveANoArgCtor() throws Exception { ScriptSource script = mock(); String badScript = "class Foo { public Foo(String foo) {}}"; given(script.getScriptAsString()).willReturn(badScript); @@ -296,7 +296,7 @@ public void testScriptedClassThatDoesNotHaveANoArgCtor() throws Exception { } @Test - public void testScriptedClassThatHasNoPublicNoArgCtor() throws Exception { + void testScriptedClassThatHasNoPublicNoArgCtor() throws Exception { ScriptSource script = mock(); String badScript = "class Foo { protected Foo() {} \n String toString() { 'X' }}"; given(script.getScriptAsString()).willReturn(badScript); @@ -306,7 +306,7 @@ public void testScriptedClassThatHasNoPublicNoArgCtor() throws Exception { } @Test - public void testWithTwoClassesDefinedInTheOneGroovyFile_CorrectClassFirst() throws Exception { + void testWithTwoClassesDefinedInTheOneGroovyFile_CorrectClassFirst() { ApplicationContext ctx = new ClassPathXmlApplicationContext("twoClassesCorrectOneFirst.xml", getClass()); Messenger messenger = (Messenger) ctx.getBean("messenger"); assertThat(messenger).isNotNull(); @@ -318,7 +318,7 @@ public void testWithTwoClassesDefinedInTheOneGroovyFile_CorrectClassFirst() thro } @Test - public void testWithTwoClassesDefinedInTheOneGroovyFile_WrongClassFirst() throws Exception { + void testWithTwoClassesDefinedInTheOneGroovyFile_WrongClassFirst() { assertThatException().as("two classes defined in GroovyScriptFactory source, non-Messenger class defined first").isThrownBy(() -> { ApplicationContext ctx = new ClassPathXmlApplicationContext("twoClassesWrongOneFirst.xml", getClass()); ctx.getBean("messenger", Messenger.class); @@ -326,32 +326,32 @@ public void testWithTwoClassesDefinedInTheOneGroovyFile_WrongClassFirst() throws } @Test - public void testCtorWithNullScriptSourceLocator() throws Exception { + void testCtorWithNullScriptSourceLocator() { assertThatIllegalArgumentException().isThrownBy(() -> new GroovyScriptFactory(null)); } @Test - public void testCtorWithEmptyScriptSourceLocator() throws Exception { + void testCtorWithEmptyScriptSourceLocator() { assertThatIllegalArgumentException().isThrownBy(() -> new GroovyScriptFactory("")); } @Test - public void testCtorWithWhitespacedScriptSourceLocator() throws Exception { + void testCtorWithWhitespacedScriptSourceLocator() { assertThatIllegalArgumentException().isThrownBy(() -> new GroovyScriptFactory("\n ")); } @Test - public void testWithInlineScriptWithLeadingWhitespace() throws Exception { + void testWithInlineScriptWithLeadingWhitespace() { assertThatExceptionOfType(BeanCreationException.class).as("'inline:' prefix was preceded by whitespace").isThrownBy(() -> new ClassPathXmlApplicationContext("lwspBadGroovyContext.xml", getClass())) .matches(ex -> ex.contains(FileNotFoundException.class)); } @Test - public void testGetScriptedObjectDoesNotChokeOnNullInterfacesBeingPassedIn() throws Exception { + void testGetScriptedObjectDoesNotChokeOnNullInterfacesBeingPassedIn() throws Exception { ScriptSource script = mock(); given(script.getScriptAsString()).willReturn("class Bar {}"); given(script.suggestedClassName()).willReturn("someName"); @@ -362,14 +362,14 @@ public void testGetScriptedObjectDoesNotChokeOnNullInterfacesBeingPassedIn() thr } @Test - public void testGetScriptedObjectDoesChokeOnNullScriptSourceBeingPassedIn() throws Exception { + void testGetScriptedObjectDoesChokeOnNullScriptSourceBeingPassedIn() { GroovyScriptFactory factory = new GroovyScriptFactory("a script source locator (doesn't matter here)"); assertThatNullPointerException().as("NullPointerException as per contract ('null' ScriptSource supplied)").isThrownBy(() -> factory.getScriptedObject(null)); } @Test - public void testResourceScriptFromTag() throws Exception { + void testResourceScriptFromTag() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); Messenger messenger = (Messenger) ctx.getBean("messenger"); CallCounter countingAspect = (CallCounter) ctx.getBean("getMessageAspect"); @@ -386,7 +386,7 @@ public void testResourceScriptFromTag() throws Exception { } @Test - public void testPrototypeScriptFromTag() throws Exception { + void testPrototypeScriptFromTag() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); @@ -402,7 +402,7 @@ public void testPrototypeScriptFromTag() throws Exception { } @Test - public void testInlineScriptFromTag() throws Exception { + void testInlineScriptFromTag() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); BeanDefinition bd = ctx.getBeanFactory().getBeanDefinition("calculator"); assertThat(ObjectUtils.containsElement(bd.getDependsOn(), "messenger")).isTrue(); @@ -413,9 +413,9 @@ public void testInlineScriptFromTag() throws Exception { } @Test - public void testRefreshableFromTag() throws Exception { + void testRefreshableFromTag() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("refreshableMessenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("refreshableMessenger"); Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger"); CallCounter countingAspect = (CallCounter) ctx.getBean("getMessageAspect"); @@ -427,14 +427,14 @@ public void testRefreshableFromTag() throws Exception { assertThat(messenger.getMessage()).isEqualTo("Hello World!"); assertThat(countingAspect.getCalls()).isEqualTo(1); - assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class)).containsValue(messenger); } @Test // SPR-6268 - public void testRefreshableFromTagProxyTargetClass() throws Exception { + public void testRefreshableFromTagProxyTargetClass() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-proxy-target-class.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("refreshableMessenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("refreshableMessenger"); Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger"); @@ -443,14 +443,14 @@ public void testRefreshableFromTagProxyTargetClass() throws Exception { assertThat(condition).isTrue(); assertThat(messenger.getMessage()).isEqualTo("Hello World!"); - assertThat(ctx.getBeansOfType(ConcreteMessenger.class).values().contains(messenger)).isTrue(); + assertThat(ctx.getBeansOfType(ConcreteMessenger.class)).containsValue((ConcreteMessenger) messenger); // Check that AnnotationUtils works with concrete proxied script classes assertThat(AnnotationUtils.findAnnotation(messenger.getClass(), Component.class)).isNotNull(); } @Test // SPR-6268 - public void testProxyTargetClassNotAllowedIfNotGroovy() throws Exception { + public void testProxyTargetClassNotAllowedIfNotGroovy() { try { new ClassPathXmlApplicationContext("groovy-with-xsd-proxy-target-class.xml", getClass()); } @@ -460,7 +460,7 @@ public void testProxyTargetClassNotAllowedIfNotGroovy() throws Exception { } @Test - public void testAnonymousScriptDetected() throws Exception { + void testAnonymousScriptDetected() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); Map beans = ctx.getBeansOfType(Messenger.class); assertThat(beans).hasSize(4); @@ -469,26 +469,26 @@ public void testAnonymousScriptDetected() throws Exception { } @Test - public void testJsr223FromTag() throws Exception { + void testJsr223FromTag() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messenger"); Messenger messenger = (Messenger) ctx.getBean("messenger"); assertThat(AopUtils.isAopProxy(messenger)).isFalse(); assertThat(messenger.getMessage()).isEqualTo("Hello World!"); } @Test - public void testJsr223FromTagWithInterface() throws Exception { + void testJsr223FromTagWithInterface() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerWithInterface")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("messengerWithInterface"); Messenger messenger = (Messenger) ctx.getBean("messengerWithInterface"); assertThat(AopUtils.isAopProxy(messenger)).isFalse(); } @Test - public void testRefreshableJsr223FromTag() throws Exception { + void testRefreshableJsr223FromTag() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("refreshableMessenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("refreshableMessenger"); Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger"); assertThat(AopUtils.isAopProxy(messenger)).isTrue(); boolean condition = messenger instanceof Refreshable; @@ -497,17 +497,17 @@ public void testRefreshableJsr223FromTag() throws Exception { } @Test - public void testInlineJsr223FromTag() throws Exception { + void testInlineJsr223FromTag() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("inlineMessenger")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("inlineMessenger"); Messenger messenger = (Messenger) ctx.getBean("inlineMessenger"); assertThat(AopUtils.isAopProxy(messenger)).isFalse(); } @Test - public void testInlineJsr223FromTagWithInterface() throws Exception { + void testInlineJsr223FromTagWithInterface() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); - assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("inlineMessengerWithInterface")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("inlineMessengerWithInterface"); Messenger messenger = (Messenger) ctx.getBean("inlineMessengerWithInterface"); assertThat(AopUtils.isAopProxy(messenger)).isFalse(); } @@ -517,7 +517,7 @@ public void testInlineJsr223FromTagWithInterface() throws Exception { * passed to a scripted bean :( */ @Test - public void testCanPassInMoreThanOneProperty() { + void testCanPassInMoreThanOneProperty() { ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-multiple-properties.xml", getClass()); TestBean tb = (TestBean) ctx.getBean("testBean"); @@ -533,12 +533,12 @@ public void testCanPassInMoreThanOneProperty() { } @Test - public void testMetaClassWithBeans() { + void testMetaClassWithBeans() { testMetaClass("org/springframework/scripting/groovy/calculators.xml"); } @Test - public void testMetaClassWithXsd() { + void testMetaClassWithXsd() { testMetaClass("org/springframework/scripting/groovy/calculators-with-xsd.xml"); } @@ -552,7 +552,7 @@ private void testMetaClass(String xmlFile) { } @Test - public void testFactoryBean() { + void testFactoryBean() { ApplicationContext context = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); Object factory = context.getBean("&factory"); boolean condition1 = factory instanceof FactoryBean; @@ -564,7 +564,7 @@ public void testFactoryBean() { } @Test - public void testRefreshableFactoryBean() { + void testRefreshableFactoryBean() { ApplicationContext context = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); Object factory = context.getBean("&refreshableFactory"); boolean condition1 = factory instanceof FactoryBean; diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java b/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java index 841605eb4277..8bcc9ffde890 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ public class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { private int countThrows = 0; @Override - public void before(Method method, Object[] objects, @Nullable Object o) throws Throwable { + public void before(Method method, Object[] objects, @Nullable Object o) { countBefore++; // System.out.println("Method:" + method.getName()); } diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/TestService.java b/spring-context/src/test/java/org/springframework/scripting/groovy/TestService.java index aedebb2e769c..75498e9d92e9 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/TestService.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/TestService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,5 +18,5 @@ public interface TestService { - public String sayHello(); + String sayHello(); } diff --git a/spring-context/src/test/java/org/springframework/scripting/support/RefreshableScriptTargetSourceTests.java b/spring-context/src/test/java/org/springframework/scripting/support/RefreshableScriptTargetSourceTests.java index c2ae23cdce89..cc8d45913a8a 100644 --- a/spring-context/src/test/java/org/springframework/scripting/support/RefreshableScriptTargetSourceTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/support/RefreshableScriptTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,10 @@ /** * @author Rick Evans */ -public class RefreshableScriptTargetSourceTests { +class RefreshableScriptTargetSourceTests { @Test - public void createWithNullScriptSource() throws Exception { + void createWithNullScriptSource() { assertThatIllegalArgumentException().isThrownBy(() -> new RefreshableScriptTargetSource(mock(), "a.bean", null, null, false)); } diff --git a/spring-context/src/test/java/org/springframework/scripting/support/ResourceScriptSourceTests.java b/spring-context/src/test/java/org/springframework/scripting/support/ResourceScriptSourceTests.java index bca03aee18c3..83d38b262471 100644 --- a/spring-context/src/test/java/org/springframework/scripting/support/ResourceScriptSourceTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/support/ResourceScriptSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ * @author Rick Evans * @author Juergen Hoeller */ -public class ResourceScriptSourceTests { +class ResourceScriptSourceTests { @Test - public void doesNotPropagateFatalExceptionOnResourceThatCannotBeResolvedToAFile() throws Exception { + void doesNotPropagateFatalExceptionOnResourceThatCannotBeResolvedToAFile() throws Exception { Resource resource = mock(); given(resource.lastModified()).willThrow(new IOException()); @@ -45,14 +45,14 @@ public void doesNotPropagateFatalExceptionOnResourceThatCannotBeResolvedToAFile( } @Test - public void beginsInModifiedState() throws Exception { + void beginsInModifiedState() { Resource resource = mock(); ResourceScriptSource scriptSource = new ResourceScriptSource(resource); assertThat(scriptSource.isModified()).isTrue(); } @Test - public void lastModifiedWorksWithResourceThatDoesNotSupportFileBasedReading() throws Exception { + void lastModifiedWorksWithResourceThatDoesNotSupportFileBasedReading() throws Exception { Resource resource = mock(); // underlying File is asked for so that the last modified time can be checked... // And then mock the file changing; i.e. the File says it has been modified @@ -71,7 +71,7 @@ public void lastModifiedWorksWithResourceThatDoesNotSupportFileBasedReading() th } @Test - public void lastModifiedWorksWithResourceThatDoesNotSupportFileBasedAccessAtAll() throws Exception { + void lastModifiedWorksWithResourceThatDoesNotSupportFileBasedAccessAtAll() throws Exception { Resource resource = new ByteArrayResource(new byte[0]); ResourceScriptSource scriptSource = new ResourceScriptSource(resource); assertThat(scriptSource.isModified()).as("ResourceScriptSource must start off in the 'isModified' state (it obviously isn't).").isTrue(); diff --git a/spring-context/src/test/java/org/springframework/scripting/support/ScriptFactoryPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scripting/support/ScriptFactoryPostProcessorTests.java index f9601ab2c3d4..32cfd5e8d73b 100644 --- a/spring-context/src/test/java/org/springframework/scripting/support/ScriptFactoryPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/support/ScriptFactoryPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,7 +91,7 @@ void testThrowsExceptionIfGivenNonAbstractBeanFactoryImplementation() { } @Test - void testChangeScriptWithRefreshableBeanFunctionality() throws Exception { + void testChangeScriptWithRefreshableBeanFunctionality() { BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(true); BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); @@ -112,7 +112,7 @@ void testChangeScriptWithRefreshableBeanFunctionality() throws Exception { } @Test - void testChangeScriptWithNoRefreshableBeanFunctionality() throws Exception { + void testChangeScriptWithNoRefreshableBeanFunctionality() { BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(false); BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); @@ -132,7 +132,7 @@ void testChangeScriptWithNoRefreshableBeanFunctionality() throws Exception { } @Test - void testRefreshedScriptReferencePropagatesToCollaborators() throws Exception { + void testRefreshedScriptReferencePropagatesToCollaborators() { BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(true); BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); BeanDefinitionBuilder collaboratorBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultMessengerService.class); @@ -161,7 +161,7 @@ void testRefreshedScriptReferencePropagatesToCollaborators() throws Exception { @Test @SuppressWarnings("resource") - void testReferencesAcrossAContainerHierarchy() throws Exception { + void testReferencesAcrossAContainerHierarchy() { GenericApplicationContext businessContext = new GenericApplicationContext(); businessContext.registerBeanDefinition("messenger", BeanDefinitionBuilder.rootBeanDefinition(StubMessenger.class).getBeanDefinition()); businessContext.refresh(); @@ -178,13 +178,13 @@ void testReferencesAcrossAContainerHierarchy() throws Exception { @Test @SuppressWarnings("resource") - void testScriptHavingAReferenceToAnotherBean() throws Exception { + void testScriptHavingAReferenceToAnotherBean() { // just tests that the (singleton) script-backed bean is able to be instantiated with references to its collaborators new ClassPathXmlApplicationContext("org/springframework/scripting/support/groovyReferences.xml"); } @Test - void testForRefreshedScriptHavingErrorPickedUpOnFirstCall() throws Exception { + void testForRefreshedScriptHavingErrorPickedUpOnFirstCall() { BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(true); BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); BeanDefinitionBuilder collaboratorBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultMessengerService.class); @@ -211,7 +211,7 @@ void testForRefreshedScriptHavingErrorPickedUpOnFirstCall() throws Exception { @Test @SuppressWarnings("resource") - void testPrototypeScriptedBean() throws Exception { + void testPrototypeScriptedBean() { GenericApplicationContext ctx = new GenericApplicationContext(); ctx.registerBeanDefinition("messenger", BeanDefinitionBuilder.rootBeanDefinition(StubMessenger.class).getBeanDefinition()); @@ -230,7 +230,7 @@ void testPrototypeScriptedBean() throws Exception { assertThat(messenger2).isNotSameAs(messenger1); } - private static StaticScriptSource getScriptSource(GenericApplicationContext ctx) throws Exception { + private static StaticScriptSource getScriptSource(GenericApplicationContext ctx) { ScriptFactoryPostProcessor processor = (ScriptFactoryPostProcessor) ctx.getBean(PROCESSOR_BEAN_NAME); BeanDefinition bd = processor.scriptBeanFactory.getBeanDefinition("scriptedObject.messenger"); return (StaticScriptSource) bd.getConstructorArgumentValues().getIndexedArgumentValue(0, StaticScriptSource.class).getValue(); @@ -263,7 +263,7 @@ public void setMessage(String message) { private static void pauseToLetRefreshDelayKickIn(int secondsToPause) { try { - Thread.sleep(secondsToPause * 1000); + Thread.sleep(secondsToPause * 1000L); } catch (InterruptedException ignored) { } diff --git a/spring-context/src/test/java/org/springframework/scripting/support/StaticScriptSourceTests.java b/spring-context/src/test/java/org/springframework/scripting/support/StaticScriptSourceTests.java index a9bddab90031..24d3f0f40bf5 100644 --- a/spring-context/src/test/java/org/springframework/scripting/support/StaticScriptSourceTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/support/StaticScriptSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,12 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the StaticScriptSource class. + * Tests for {@link StaticScriptSource}. * * @author Rick Evans * @author Sam Brannen */ -public class StaticScriptSourceTests { +class StaticScriptSourceTests { private static final String SCRIPT_TEXT = "print($hello) if $true;"; @@ -35,49 +35,49 @@ public class StaticScriptSourceTests { @Test - public void createWithNullScript() throws Exception { + void createWithNullScript() { assertThatIllegalArgumentException().isThrownBy(() -> new StaticScriptSource(null)); } @Test - public void createWithEmptyScript() throws Exception { + void createWithEmptyScript() { assertThatIllegalArgumentException().isThrownBy(() -> new StaticScriptSource("")); } @Test - public void createWithWhitespaceOnlyScript() throws Exception { + void createWithWhitespaceOnlyScript() { assertThatIllegalArgumentException().isThrownBy(() -> new StaticScriptSource(" \n\n\t \t\n")); } @Test - public void isModifiedIsTrueByDefault() throws Exception { + void isModifiedIsTrueByDefault() { assertThat(source.isModified()).as("Script must be flagged as 'modified' when first created.").isTrue(); } @Test - public void gettingScriptTogglesIsModified() throws Exception { + void gettingScriptTogglesIsModified() { source.getScriptAsString(); assertThat(source.isModified()).as("Script must be flagged as 'not modified' after script is read.").isFalse(); } @Test - public void gettingScriptViaToStringDoesNotToggleIsModified() throws Exception { + void gettingScriptViaToStringDoesNotToggleIsModified() { boolean isModifiedState = source.isModified(); source.toString(); assertThat(source.isModified()).as("Script's 'modified' flag must not change after script is read via toString().").isEqualTo(isModifiedState); } @Test - public void isModifiedToggledWhenDifferentScriptIsSet() throws Exception { + void isModifiedToggledWhenDifferentScriptIsSet() { source.setScript("use warnings;"); assertThat(source.isModified()).as("Script must be flagged as 'modified' when different script is passed in.").isTrue(); } @Test - public void isModifiedNotToggledWhenSameScriptIsSet() throws Exception { + void isModifiedNotToggledWhenSameScriptIsSet() { source.setScript(SCRIPT_TEXT); assertThat(source.isModified()).as("Script must not be flagged as 'modified' when same script is passed in.").isFalse(); } diff --git a/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java b/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java index 0909a7b8d427..5c36f02ce4f2 100644 --- a/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java +++ b/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,10 +41,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class ModelMapTests { +class ModelMapTests { @Test - public void testNoArgCtorYieldsEmptyModel() throws Exception { + void testNoArgCtorYieldsEmptyModel() { assertThat(new ModelMap()).isEmpty(); } @@ -52,7 +52,7 @@ public void testNoArgCtorYieldsEmptyModel() throws Exception { * SPR-2185 - Null model assertion causes backwards compatibility issue */ @Test - public void testAddNullObjectWithExplicitKey() throws Exception { + void testAddNullObjectWithExplicitKey() { ModelMap model = new ModelMap(); model.addAttribute("foo", null); assertThat(model.containsKey("foo")).isTrue(); @@ -63,14 +63,14 @@ public void testAddNullObjectWithExplicitKey() throws Exception { * SPR-2185 - Null model assertion causes backwards compatibility issue */ @Test - public void testAddNullObjectViaCtorWithExplicitKey() throws Exception { + void testAddNullObjectViaCtorWithExplicitKey() { ModelMap model = new ModelMap("foo", null); assertThat(model.containsKey("foo")).isTrue(); assertThat(model.get("foo")).isNull(); } @Test - public void testNamedObjectCtor() throws Exception { + void testNamedObjectCtor() { ModelMap model = new ModelMap("foo", "bing"); assertThat(model).hasSize(1); String bing = (String) model.get("foo"); @@ -79,7 +79,7 @@ public void testNamedObjectCtor() throws Exception { } @Test - public void testUnnamedCtorScalar() throws Exception { + void testUnnamedCtorScalar() { ModelMap model = new ModelMap("foo", "bing"); assertThat(model).hasSize(1); String bing = (String) model.get("foo"); @@ -88,7 +88,7 @@ public void testUnnamedCtorScalar() throws Exception { } @Test - public void testOneArgCtorWithScalar() throws Exception { + void testOneArgCtorWithScalar() { ModelMap model = new ModelMap("bing"); assertThat(model).hasSize(1); String string = (String) model.get("string"); @@ -97,14 +97,14 @@ public void testOneArgCtorWithScalar() throws Exception { } @Test - public void testOneArgCtorWithNull() { + void testOneArgCtorWithNull() { //Null model arguments added without a name being explicitly supplied are not allowed assertThatIllegalArgumentException().isThrownBy(() -> new ModelMap(null)); } @Test - public void testOneArgCtorWithCollection() throws Exception { + void testOneArgCtorWithCollection() { ModelMap model = new ModelMap(new String[]{"foo", "boing"}); assertThat(model).hasSize(1); String[] strings = (String[]) model.get("stringList"); @@ -115,14 +115,14 @@ public void testOneArgCtorWithCollection() throws Exception { } @Test - public void testOneArgCtorWithEmptyCollection() throws Exception { + void testOneArgCtorWithEmptyCollection() { ModelMap model = new ModelMap(new HashSet<>()); // must not add if collection is empty... assertThat(model).isEmpty(); } @Test - public void testAddObjectWithNull() throws Exception { + void testAddObjectWithNull() { // Null model arguments added without a name being explicitly supplied are not allowed ModelMap model = new ModelMap(); assertThatIllegalArgumentException().isThrownBy(() -> @@ -130,7 +130,7 @@ public void testAddObjectWithNull() throws Exception { } @Test - public void testAddObjectWithEmptyArray() throws Exception { + void testAddObjectWithEmptyArray() { ModelMap model = new ModelMap(new int[]{}); assertThat(model).hasSize(1); int[] ints = (int[]) model.get("intList"); @@ -139,21 +139,21 @@ public void testAddObjectWithEmptyArray() throws Exception { } @Test - public void testAddAllObjectsWithNullMap() throws Exception { + void testAddAllObjectsWithNullMap() { ModelMap model = new ModelMap(); model.addAllAttributes((Map) null); assertThat(model).isEmpty(); } @Test - public void testAddAllObjectsWithNullCollection() throws Exception { + void testAddAllObjectsWithNullCollection() { ModelMap model = new ModelMap(); model.addAllAttributes((Collection) null); assertThat(model).isEmpty(); } @Test - public void testAddAllObjectsWithSparseArrayList() throws Exception { + void testAddAllObjectsWithSparseArrayList() { // Null model arguments added without a name being explicitly supplied are not allowed ModelMap model = new ModelMap(); ArrayList list = new ArrayList<>(); @@ -164,7 +164,7 @@ public void testAddAllObjectsWithSparseArrayList() throws Exception { } @Test - public void testAddMap() throws Exception { + void testAddMap() { Map map = new HashMap<>(); map.put("one", "one-value"); map.put("two", "two-value"); @@ -176,7 +176,7 @@ public void testAddMap() throws Exception { } @Test - public void testAddObjectNoKeyOfSameTypeOverrides() throws Exception { + void testAddObjectNoKeyOfSameTypeOverrides() { ModelMap model = new ModelMap(); model.addAttribute("foo"); model.addAttribute("bar"); @@ -186,7 +186,7 @@ public void testAddObjectNoKeyOfSameTypeOverrides() throws Exception { } @Test - public void testAddListOfTheSameObjects() throws Exception { + void testAddListOfTheSameObjects() { List beans = new ArrayList<>(); beans.add(new TestBean("one")); beans.add(new TestBean("two")); @@ -197,7 +197,7 @@ public void testAddListOfTheSameObjects() throws Exception { } @Test - public void testMergeMapWithOverriding() throws Exception { + void testMergeMapWithOverriding() { Map beans = new HashMap<>(); beans.put("one", new TestBean("one")); beans.put("two", new TestBean("two")); @@ -210,7 +210,7 @@ public void testMergeMapWithOverriding() throws Exception { } @Test - public void testInnerClass() throws Exception { + void testInnerClass() { ModelMap map = new ModelMap(); SomeInnerClass inner = new SomeInnerClass(); map.addAttribute(inner); @@ -218,7 +218,7 @@ public void testInnerClass() throws Exception { } @Test - public void testInnerClassWithTwoUpperCaseLetters() throws Exception { + void testInnerClassWithTwoUpperCaseLetters() { ModelMap map = new ModelMap(); UKInnerClass inner = new UKInnerClass(); map.addAttribute(inner); @@ -226,7 +226,7 @@ public void testInnerClassWithTwoUpperCaseLetters() throws Exception { } @Test - public void testAopCglibProxy() throws Exception { + void testAopCglibProxy() { ModelMap map = new ModelMap(); ProxyFactory factory = new ProxyFactory(); SomeInnerClass val = new SomeInnerClass(); @@ -238,7 +238,7 @@ public void testAopCglibProxy() throws Exception { } @Test - public void testAopJdkProxy() throws Exception { + void testAopJdkProxy() { ModelMap map = new ModelMap(); ProxyFactory factory = new ProxyFactory(); Map target = new HashMap<>(); @@ -250,7 +250,7 @@ public void testAopJdkProxy() throws Exception { } @Test - public void testAopJdkProxyWithMultipleInterfaces() throws Exception { + void testAopJdkProxyWithMultipleInterfaces() { ModelMap map = new ModelMap(); Map target = new HashMap<>(); ProxyFactory factory = new ProxyFactory(); @@ -265,7 +265,7 @@ public void testAopJdkProxyWithMultipleInterfaces() throws Exception { } @Test - public void testAopJdkProxyWithDetectedInterfaces() throws Exception { + void testAopJdkProxyWithDetectedInterfaces() { ModelMap map = new ModelMap(); Map target = new HashMap<>(); ProxyFactory factory = new ProxyFactory(target); @@ -275,7 +275,7 @@ public void testAopJdkProxyWithDetectedInterfaces() throws Exception { } @Test - public void testRawJdkProxy() throws Exception { + void testRawJdkProxy() { ModelMap map = new ModelMap(); Object proxy = Proxy.newProxyInstance( getClass().getClassLoader(), diff --git a/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java b/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java index 900184a75fa0..1530e37a1e4e 100644 --- a/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java +++ b/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ public class MBeanTestUtils { * {@linkplain #releaseMBeanServer(MBeanServer) releasing} all currently registered * MBeanServers. */ - public static synchronized void resetMBeanServers() throws Exception { + public static synchronized void resetMBeanServers() { for (MBeanServer server : MBeanServerFactory.findMBeanServer(null)) { releaseMBeanServer(server); } diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java new file mode 100644 index 000000000000..9eb00934def4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation; + +import java.beans.ConstructorProperties; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import jakarta.validation.constraints.NotNull; +import org.junit.jupiter.api.Test; + +import org.springframework.core.ResolvableType; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DataBinder} with constructor binding. + * + * @author Rossen Stoyanchev + */ +class DataBinderConstructTests { + + @Test + void dataClassBinding() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("param1", "value1", "param2", "true")); + DataBinder binder = initDataBinder(DataClass.class); + binder.construct(valueResolver); + + DataClass dataClass = getTarget(binder); + assertThat(dataClass.param1()).isEqualTo("value1"); + assertThat(dataClass.param2()).isEqualTo(true); + assertThat(dataClass.param3()).isEqualTo(0); + } + + @Test + void dataClassBindingWithOptionalParameter() { + MapValueResolver valueResolver = + new MapValueResolver(Map.of("param1", "value1", "param2", "true", "optionalParam", "8")); + + DataBinder binder = initDataBinder(DataClass.class); + binder.construct(valueResolver); + + DataClass dataClass = getTarget(binder); + assertThat(dataClass.param1()).isEqualTo("value1"); + assertThat(dataClass.param2()).isEqualTo(true); + assertThat(dataClass.param3()).isEqualTo(8); + } + + @Test + void dataClassBindingWithMissingParameter() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("param1", "value1")); + DataBinder binder = initDataBinder(DataClass.class); + binder.construct(valueResolver); + + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getAllErrors()).hasSize(1); + assertThat(bindingResult.getFieldValue("param1")).isEqualTo("value1"); + assertThat(bindingResult.getFieldValue("param2")).isNull(); + assertThat(bindingResult.getFieldValue("param3")).isNull(); + } + + @Test // gh-31821 + void dataClassBindingWithNestedOptionalParameterWithMissingParameter() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("param1", "value1")); + DataBinder binder = initDataBinder(NestedDataClass.class); + binder.construct(valueResolver); + + NestedDataClass dataClass = getTarget(binder); + assertThat(dataClass.param1()).isEqualTo("value1"); + assertThat(dataClass.nestedParam2()).isNull(); + } + + @Test + void dataClassBindingWithConversionError() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("param1", "value1", "param2", "x")); + DataBinder binder = initDataBinder(DataClass.class); + binder.construct(valueResolver); + + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getAllErrors()).hasSize(1); + assertThat(bindingResult.getFieldValue("param1")).isEqualTo("value1"); + assertThat(bindingResult.getFieldValue("param2")).isEqualTo("x"); + assertThat(bindingResult.getFieldValue("param3")).isNull(); + } + + @SuppressWarnings("SameParameterValue") + private static DataBinder initDataBinder(Class targetType) { + DataBinder binder = new DataBinder(null); + binder.setTargetType(ResolvableType.forClass(targetType)); + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + } + + @SuppressWarnings("unchecked") + private static T getTarget(DataBinder dataBinder) { + assertThat(dataBinder.getBindingResult().getAllErrors()).isEmpty(); + Object target = dataBinder.getTarget(); + assertThat(target).isNotNull(); + return (T) target; + } + + + private static class DataClass { + + @NotNull + private final String param1; + + private final boolean param2; + + private int param3; + + @ConstructorProperties({"param1", "param2", "optionalParam"}) + DataClass(String param1, boolean p2, Optional optionalParam) { + this.param1 = param1; + this.param2 = p2; + Assert.notNull(optionalParam, "Optional must not be null"); + optionalParam.ifPresent(integer -> this.param3 = integer); + } + + public String param1() { + return this.param1; + } + + public boolean param2() { + return this.param2; + } + + public int param3() { + return this.param3; + } + } + + + private static class NestedDataClass { + + private final String param1; + + @Nullable + private final DataClass nestedParam2; + + public NestedDataClass(String param1, @Nullable DataClass nestedParam2) { + this.param1 = param1; + this.nestedParam2 = nestedParam2; + } + + public String param1() { + return this.param1; + } + + @Nullable + public DataClass nestedParam2() { + return this.nestedParam2; + } + } + + + private static class MapValueResolver implements DataBinder.ValueResolver { + + private final Map map; + + private MapValueResolver(Map map) { + this.map = map; + } + + @Override + public Object resolveValue(String name, Class type) { + return map.get(name); + } + + @Override + public Set getNames() { + return this.map.keySet(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderFieldAccessTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderFieldAccessTests.java index d1eea63691eb..570308e55357 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderFieldAccessTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderFieldAccessTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,7 +62,7 @@ void bindingNoErrors() throws Exception { } @Test - void bindingNoErrorsNotIgnoreUnknown() throws Exception { + void bindingNoErrorsNotIgnoreUnknown() { FieldAccessBean rod = new FieldAccessBean(); DataBinder binder = new DataBinder(rod, "person"); binder.initDirectFieldAccess(); @@ -76,7 +76,7 @@ void bindingNoErrorsNotIgnoreUnknown() throws Exception { } @Test - void bindingWithErrors() throws Exception { + void bindingWithErrors() { FieldAccessBean rod = new FieldAccessBean(); DataBinder binder = new DataBinder(rod, "person"); binder.initDirectFieldAccess(); @@ -122,7 +122,7 @@ void nestedBindingWithDefaultConversionNoErrors() throws Exception { } @Test - void nestedBindingWithDisabledAutoGrow() throws Exception { + void nestedBindingWithDisabledAutoGrow() { FieldAccessBean rod = new FieldAccessBean(); DataBinder binder = new DataBinder(rod, "person"); binder.setAutoGrowNestedPaths(false); @@ -135,7 +135,7 @@ void nestedBindingWithDisabledAutoGrow() throws Exception { } @Test - void bindingWithErrorsAndCustomEditors() throws Exception { + void bindingWithErrorsAndCustomEditors() { FieldAccessBean rod = new FieldAccessBean(); DataBinder binder = new DataBinder(rod, "person"); binder.initDirectFieldAccess(); diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 037dc8d214a3..e34cbe78b2f3 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import java.util.Set; import java.util.TreeSet; +import org.assertj.core.api.IntegerAssert; import org.junit.jupiter.api.Test; import org.springframework.beans.BeanWrapper; @@ -72,16 +73,28 @@ import static org.assertj.core.api.Assertions.entry; /** - * Unit tests for {@link DataBinder}. + * Tests for {@link DataBinder}. * * @author Rod Johnson * @author Juergen Hoeller * @author Rob Harrop * @author Kazuki Shimizu * @author Sam Brannen + * @author Arjen Poutsma */ class DataBinderTests { + private final Validator spouseValidator = Validator.forInstanceOf(TestBean.class, (tb, errors) -> { + if (tb == null || "XXX".equals(tb.getName())) { + errors.rejectValue("", "SPOUSE_NOT_AVAILABLE"); + return; + } + if (tb.getAge() < 32) { + errors.rejectValue("age", "TOO_YOUNG", "simply too young"); + } + }); + + @Test void bindingNoErrors() throws BindException { TestBean rod = new TestBean(); @@ -103,7 +116,7 @@ void bindingNoErrors() throws BindException { TestBean tb = (TestBean) map.get("person"); assertThat(tb.equals(rod)).as("Same object").isTrue(); - BindingResult other = new BeanPropertyBindingResult(rod, "person"); + BindingResult other = new DataBinder(rod, "person").getBindingResult(); assertThat(binder.getBindingResult()).isEqualTo(other); assertThat(other).isEqualTo(binder.getBindingResult()); BindException ex = new BindException(other); @@ -435,7 +448,7 @@ void bindingErrorWithRuntimeExceptionFromFormatter() { conversionService.addFormatter(new Formatter() { @Override - public String parse(String text, Locale locale) throws ParseException { + public String parse(String text, Locale locale) { throw new RuntimeException(text); } @Override @@ -468,7 +481,7 @@ void bindingWithFormatterAgainstList() { LocaleContextHolder.setLocale(Locale.GERMAN); try { binder.bind(pvs); - assertThat(tb.getIntegerList().get(0)).isEqualTo(1); + assertThat(tb.getIntegerList()).containsExactly(1); assertThat(binder.getBindingResult().getFieldValue("integerList[0]")).isEqualTo("1"); } finally { @@ -490,7 +503,7 @@ void bindingErrorWithFormatterAgainstList() { LocaleContextHolder.setLocale(Locale.GERMAN); try { binder.bind(pvs); - assertThat(tb.getIntegerList().isEmpty()).isTrue(); + assertThat(tb.getIntegerList()).isEmpty(); assertThat(binder.getBindingResult().getFieldValue("integerList[0]")).isEqualTo("1x2"); assertThat(binder.getBindingResult().hasFieldErrors("integerList[0]")).isTrue(); } @@ -638,7 +651,7 @@ void bindingErrorWithRuntimeExceptionFromCustomFormatter() { binder.addCustomFormatter(new Formatter() { @Override - public String parse(String text, Locale locale) throws ParseException { + public String parse(String text, Locale locale) { throw new RuntimeException(text); } @Override @@ -669,6 +682,23 @@ void conversionWithInappropriateStringEditor() { assertThat(dataBinder.convertIfNecessary(bean, String.class)).as("Type converter should have been used").isEqualTo("[Fred]"); } + @Test + void bindingInDeclarativeMode() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod); + binder.setDeclarativeBinding(true); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "32x"); + + binder.bind(pvs); + binder.close(); + + assertThat(rod.getName()).isNull(); + assertThat(rod.getAge()).isEqualTo(0); + } + @Test void bindingWithAllowedFields() throws BindException { TestBean rod = new TestBean(); @@ -993,7 +1023,7 @@ void customFormatterForSingleProperty() { binder.addCustomFormatter(new Formatter() { @Override - public String parse(String text, Locale locale) throws ParseException { + public String parse(String text, Locale locale) { return "prefix" + text; } @Override @@ -1032,7 +1062,7 @@ void customFormatterForPrimitiveProperty() { binder.addCustomFormatter(new Formatter() { @Override - public Integer parse(String text, Locale locale) throws ParseException { + public Integer parse(String text, Locale locale) { return 99; } @Override @@ -1056,7 +1086,7 @@ void customFormatterForAllStringProperties() { binder.addCustomFormatter(new Formatter() { @Override - public String parse(String text, Locale locale) throws ParseException { + public String parse(String text, Locale locale) { return "prefix" + text; } @Override @@ -1145,7 +1175,6 @@ void validatorNoErrors() throws Exception { errors.setNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); assertThat(errors.getFieldValue("age")).isEqualTo("argh"); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1195,7 +1224,6 @@ void validatorWithErrors() { errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1272,7 +1300,6 @@ void validatorWithErrorsAndCodesPrefix() { errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1333,6 +1360,55 @@ void validatorWithErrorsAndCodesPrefix() { assertThat((errors.getFieldErrors("spouse.age").get(0)).getRejectedValue()).isEqualTo(0); } + @Test + void validateObjectWithErrors() { + TestBean tb = new TestBean(); + Errors errors = new SimpleErrors(tb, "tb"); + + Validator testValidator = new TestBeanValidator(); + testValidator.validate(tb, errors); + + assertThat(errors.hasErrors()).isTrue(); + assertThat(errors.getErrorCount()).isEqualTo(5); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + + assertThat(errors.hasGlobalErrors()).isTrue(); + assertThat(errors.getGlobalErrorCount()).isEqualTo(2); + assertThat(errors.getGlobalError().getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getGlobalErrors().get(1)).getCode()).isEqualTo("GENERAL_ERROR"); + assertThat((errors.getGlobalErrors().get(1)).getDefaultMessage()).isEqualTo("msg"); + assertThat((errors.getGlobalErrors().get(1)).getArguments()[0]).isEqualTo("arg"); + + assertThat(errors.hasFieldErrors()).isTrue(); + assertThat(errors.getFieldErrorCount()).isEqualTo(3); + assertThat(errors.getFieldError().getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(1)).getCode()).isEqualTo("AGE_NOT_ODD"); + assertThat((errors.getFieldErrors().get(1)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(2)).getCode()).isEqualTo("NOT_ROD"); + assertThat((errors.getFieldErrors().get(2)).getField()).isEqualTo("name"); + + assertThat(errors.hasFieldErrors("age")).isTrue(); + assertThat(errors.getFieldErrorCount("age")).isEqualTo(2); + assertThat(errors.getFieldError("age").getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getFieldErrors("age").get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors("age").get(0)).getRejectedValue()).isEqualTo(0); + assertThat((errors.getFieldErrors("age").get(1)).getCode()).isEqualTo("AGE_NOT_ODD"); + + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldErrorCount("name")).isEqualTo(1); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("NOT_ROD"); + assertThat((errors.getFieldErrors("name").get(0)).getField()).isEqualTo("name"); + assertThat((errors.getFieldErrors("name").get(0)).getRejectedValue()).isNull(); + } + @Test void validatorWithNestedObjectNull() { TestBean tb = new TestBean(); @@ -1343,7 +1419,6 @@ void validatorWithNestedObjectNull() { errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1358,14 +1433,12 @@ void validatorWithNestedObjectNull() { void nestedValidatorWithoutNestedPath() { TestBean tb = new TestBean(); tb.setName("XXX"); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - Validator spouseValidator = new SpouseValidator(); - spouseValidator.validate(tb, errors); + Errors errors = spouseValidator.validateObject(tb); assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(1); assertThat(errors.getGlobalError().getCode()).isEqualTo("SPOUSE_NOT_AVAILABLE"); - assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("TestBean"); } @Test @@ -1776,7 +1849,7 @@ void addAllErrors() { binder.bind(pvs); Errors errors = binder.getBindingResult(); - BeanPropertyBindingResult errors2 = new BeanPropertyBindingResult(rod, "person"); + Errors errors2 = new SimpleErrors(rod, "person"); errors.rejectValue("name", "badName"); errors.addAllErrors(errors2); @@ -1811,7 +1884,7 @@ void rejectWithoutDefaultMessage() { tb.setName("myName"); tb.setAge(99); - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb, "tb"); errors.reject("invalid"); errors.rejectValue("age", "invalidField"); @@ -1935,9 +2008,7 @@ void nestedGrowingList() { assertThat(binder.getBindingResult().hasErrors()).isFalse(); @SuppressWarnings("unchecked") List list = (List) form.getF().get("list"); - assertThat(list.get(0)).isEqualTo("firstValue"); - assertThat(list.get(1)).isEqualTo("secondValue"); - assertThat(list).hasSize(2); + assertThat(list).containsExactly("firstValue", "secondValue"); } @Test @@ -1971,7 +2042,7 @@ void setAutoGrowCollectionLimit() { binder.bind(pvs); assertThat(tb.getIntegerList()).hasSize(257); - assertThat(tb.getIntegerList().get(256)).isEqualTo(Integer.valueOf(1)); + assertThat(tb.getIntegerList(), IntegerAssert.class).element(256).isEqualTo(1); assertThat(binder.getBindingResult().getFieldValue("integerList[256]")).isEqualTo(1); } @@ -1984,7 +2055,7 @@ void setAutoGrowCollectionLimitAfterInitialization() { .withMessageContaining("DataBinder is already initialized - call setAutoGrowCollectionLimit before other configuration methods"); } - @Test // SPR-15009 + @Test // SPR-15009 void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -2001,7 +2072,7 @@ void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAc assertThat(((BeanWrapper) binder.getInternalBindingResult().getPropertyAccessor()).getAutoGrowCollectionLimit()).isEqualTo(512); } - @Test // SPR-15009 + @Test // SPR-15009 void setCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -2055,7 +2126,7 @@ void callSetMessageCodesResolverTwice() { .withMessageContaining("DataBinder is already initialized with MessageCodesResolver"); } - @Test // gh-24347 + @Test // gh-24347 void overrideBindingResultType() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -2172,27 +2243,6 @@ public void validate(@Nullable Object obj, Errors errors) { } - private static class SpouseValidator implements Validator { - - @Override - public boolean supports(Class clazz) { - return TestBean.class.isAssignableFrom(clazz); - } - - @Override - public void validate(@Nullable Object obj, Errors errors) { - TestBean tb = (TestBean) obj; - if (tb == null || "XXX".equals(tb.getName())) { - errors.rejectValue("", "SPOUSE_NOT_AVAILABLE"); - return; - } - if (tb.getAge() < 32) { - errors.rejectValue("age", "TOO_YOUNG", "simply too young"); - } - } - } - - @SuppressWarnings("unused") private static class GrowingList extends AbstractList { diff --git a/spring-context/src/test/java/org/springframework/validation/DefaultMessageCodesResolverTests.java b/spring-context/src/test/java/org/springframework/validation/DefaultMessageCodesResolverTests.java index 962ea0c80687..d37241fcd8f3 100644 --- a/spring-context/src/test/java/org/springframework/validation/DefaultMessageCodesResolverTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DefaultMessageCodesResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,13 +34,13 @@ class DefaultMessageCodesResolverTests { @Test - void shouldResolveMessageCode() throws Exception { + void shouldResolveMessageCode() { String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); assertThat(codes).containsExactly("errorCode.objectName", "errorCode"); } @Test - void shouldResolveFieldMessageCode() throws Exception { + void shouldResolveFieldMessageCode() { String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); assertThat(codes).containsExactly( "errorCode.objectName.field", @@ -50,7 +50,7 @@ void shouldResolveFieldMessageCode() throws Exception { } @Test - void shouldResolveIndexedFieldMessageCode() throws Exception { + void shouldResolveIndexedFieldMessageCode() { String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "a.b[3].c[5].d", TestBean.class); assertThat(codes).containsExactly( "errorCode.objectName.a.b[3].c[5].d", @@ -65,14 +65,14 @@ void shouldResolveIndexedFieldMessageCode() throws Exception { } @Test - void shouldResolveMessageCodeWithPrefix() throws Exception { + void shouldResolveMessageCodeWithPrefix() { resolver.setPrefix("prefix."); String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); assertThat(codes).containsExactly("prefix.errorCode.objectName", "prefix.errorCode"); } @Test - void shouldResolveFieldMessageCodeWithPrefix() throws Exception { + void shouldResolveFieldMessageCodeWithPrefix() { resolver.setPrefix("prefix."); String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); assertThat(codes).containsExactly( @@ -83,7 +83,7 @@ void shouldResolveFieldMessageCodeWithPrefix() throws Exception { } @Test - void shouldSupportNullPrefix() throws Exception { + void shouldSupportNullPrefix() { resolver.setPrefix(null); String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); assertThat(codes).containsExactly( @@ -94,7 +94,7 @@ void shouldSupportNullPrefix() throws Exception { } @Test - void shouldSupportMalformedIndexField() throws Exception { + void shouldSupportMalformedIndexField() { String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field[", TestBean.class); assertThat(codes).containsExactly( "errorCode.objectName.field[", @@ -104,7 +104,7 @@ void shouldSupportMalformedIndexField() throws Exception { } @Test - void shouldSupportNullFieldType() throws Exception { + void shouldSupportNullFieldType() { String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", null); assertThat(codes).containsExactly( "errorCode.objectName.field", @@ -113,14 +113,14 @@ void shouldSupportNullFieldType() throws Exception { } @Test - void shouldSupportPostfixFormat() throws Exception { + void shouldSupportPostfixFormat() { resolver.setMessageCodeFormatter(Format.POSTFIX_ERROR_CODE); String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); assertThat(codes).containsExactly("objectName.errorCode", "errorCode"); } @Test - void shouldSupportFieldPostfixFormat() throws Exception { + void shouldSupportFieldPostfixFormat() { resolver.setMessageCodeFormatter(Format.POSTFIX_ERROR_CODE); String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); assertThat(codes).containsExactly( @@ -131,7 +131,7 @@ void shouldSupportFieldPostfixFormat() throws Exception { } @Test - void shouldSupportCustomFormat() throws Exception { + void shouldSupportCustomFormat() { resolver.setMessageCodeFormatter((errorCode, objectName, field) -> DefaultMessageCodesResolver.Format.toDelimitedString("CUSTOM-" + errorCode, objectName, field)); String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); diff --git a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java index 1131a2d645c6..d26521b917cf 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,92 +19,104 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for {@link ValidationUtils}. + * Tests for {@link ValidationUtils}. * * @author Juergen Hoeller * @author Rick Evans * @author Chris Beams + * @author Arjen Poutsma * @since 08.10.2004 */ -public class ValidationUtilsTests { +class ValidationUtilsTests { + + private final Validator emptyValidator = Validator.forInstanceOf(TestBean.class, (testBean, errors) -> + ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!")); + + private final Validator emptyOrWhitespaceValidator = Validator.forInstanceOf(TestBean.class, (testBean, errors) -> + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", "You must enter a name!")); + @Test - public void testInvokeValidatorWithNullValidator() { + void testInvokeValidatorWithNullValidator() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb); assertThatIllegalArgumentException().isThrownBy(() -> ValidationUtils.invokeValidator(null, tb, errors)); } @Test - public void testInvokeValidatorWithNullErrors() { + void testInvokeValidatorWithNullErrors() { TestBean tb = new TestBean(); assertThatIllegalArgumentException().isThrownBy(() -> - ValidationUtils.invokeValidator(new EmptyValidator(), tb, null)); + ValidationUtils.invokeValidator(emptyValidator, tb, null)); } @Test - public void testInvokeValidatorSunnyDay() { + void testInvokeValidatorSunnyDay() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - ValidationUtils.invokeValidator(new EmptyValidator(), tb, errors); + Errors errors = new SimpleErrors(tb); + ValidationUtils.invokeValidator(emptyValidator, tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); } @Test - public void testValidationUtilsSunnyDay() { + void testValidationUtilsSunnyDay() { TestBean tb = new TestBean(""); - Validator testValidator = new EmptyValidator(); tb.setName(" "); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + Errors errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isFalse(); tb.setName("Roddy"); - errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isFalse(); + + // Should not raise exception + errors.failOnError(IllegalStateException::new); } @Test - public void testValidationUtilsNull() { + void testValidationUtilsNull() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - Validator testValidator = new EmptyValidator(); - testValidator.validate(tb, errors); + Errors errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); + + assertThatIllegalStateException() + .isThrownBy(() -> errors.failOnError(IllegalStateException::new)) + .withMessageContaining("'name'").withMessageContaining("EMPTY"); } @Test - public void testValidationUtilsEmpty() { + void testValidationUtilsEmpty() { TestBean tb = new TestBean(""); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - Validator testValidator = new EmptyValidator(); - testValidator.validate(tb, errors); + Errors errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); + + assertThatIllegalStateException() + .isThrownBy(() -> errors.failOnError(IllegalStateException::new)) + .withMessageContaining("'name'").withMessageContaining("EMPTY"); } @Test - public void testValidationUtilsEmptyVariants() { + void testValidationUtilsEmptyVariants() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); - errors = new BeanPropertyBindingResult(tb, "tb"); + errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}, "msg"); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); @@ -113,49 +125,44 @@ public void testValidationUtilsEmptyVariants() { } @Test - public void testValidationUtilsEmptyOrWhitespace() { + void testValidationUtilsEmptyOrWhitespace() { TestBean tb = new TestBean(); - Validator testValidator = new EmptyOrWhitespaceValidator(); // Test null - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + Errors errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test empty String tb.setName(""); - errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test whitespace String tb.setName(" "); - errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test OK tb.setName("Roddy"); - errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isFalse(); } @Test - public void testValidationUtilsEmptyOrWhitespaceVariants() { + void testValidationUtilsEmptyOrWhitespaceVariants() { TestBean tb = new TestBean(); tb.setName(" "); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); - errors = new BeanPropertyBindingResult(tb, "tb"); + errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}, "msg"); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); @@ -163,32 +170,4 @@ public void testValidationUtilsEmptyOrWhitespaceVariants() { assertThat(errors.getFieldError("name").getDefaultMessage()).isEqualTo("msg"); } - - private static class EmptyValidator implements Validator { - - @Override - public boolean supports(Class clazz) { - return TestBean.class.isAssignableFrom(clazz); - } - - @Override - public void validate(@Nullable Object obj, Errors errors) { - ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!"); - } - } - - - private static class EmptyOrWhitespaceValidator implements Validator { - - @Override - public boolean supports(Class clazz) { - return TestBean.class.isAssignableFrom(clazz); - } - - @Override - public void validate(@Nullable Object obj, Errors errors) { - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", "You must enter a name!"); - } - } - } diff --git a/spring-context/src/test/java/org/springframework/validation/ValidatorTests.java b/spring-context/src/test/java/org/springframework/validation/ValidatorTests.java new file mode 100644 index 000000000000..b3007580b740 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/ValidatorTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class ValidatorTests { + + @Test + void testSupportsForInstanceOf() { + Validator validator = Validator.forInstanceOf(TestBean.class, (testBean, errors) -> {}); + assertThat(validator.supports(TestBean.class)).isTrue(); + assertThat(validator.supports(TestBeanSubclass.class)).isTrue(); + } + + @Test + void testSupportsForType() { + Validator validator = Validator.forType(TestBean.class, (testBean, errors) -> {}); + assertThat(validator.supports(TestBean.class)).isTrue(); + assertThat(validator.supports(TestBeanSubclass.class)).isFalse(); + } + + + private static class TestBeanSubclass extends TestBean { + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessorTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessorTests.java index cc6b89e9db87..67ac244cb499 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,10 +36,10 @@ /** * @author Juergen Hoeller */ -public class BeanValidationPostProcessorTests { +class BeanValidationPostProcessorTests { @Test - public void testNotNullConstraint() { + void testNotNullConstraint() { GenericApplicationContext ac = new GenericApplicationContext(); ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); @@ -52,7 +52,7 @@ public void testNotNullConstraint() { } @Test - public void testNotNullConstraintSatisfied() { + void testNotNullConstraintSatisfied() { GenericApplicationContext ac = new GenericApplicationContext(); ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); @@ -64,7 +64,7 @@ public void testNotNullConstraintSatisfied() { } @Test - public void testNotNullConstraintAfterInitialization() { + void testNotNullConstraintAfterInitialization() { GenericApplicationContext ac = new GenericApplicationContext(); RootBeanDefinition bvpp = new RootBeanDefinition(BeanValidationPostProcessor.class); bvpp.getPropertyValues().add("afterInitialization", true); @@ -76,7 +76,7 @@ public void testNotNullConstraintAfterInitialization() { } @Test - public void testNotNullConstraintAfterInitializationWithProxy() { + void testNotNullConstraintAfterInitializationWithProxy() { GenericApplicationContext ac = new GenericApplicationContext(); RootBeanDefinition bvpp = new RootBeanDefinition(BeanValidationPostProcessor.class); bvpp.getPropertyValues().add("afterInitialization", true); @@ -90,7 +90,7 @@ public void testNotNullConstraintAfterInitializationWithProxy() { } @Test - public void testSizeConstraint() { + void testSizeConstraint() { GenericApplicationContext ac = new GenericApplicationContext(); ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); @@ -105,7 +105,7 @@ public void testSizeConstraint() { } @Test - public void testSizeConstraintSatisfied() { + void testSizeConstraintSatisfied() { GenericApplicationContext ac = new GenericApplicationContext(); ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterPropertyPathTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterPropertyPathTests.java new file mode 100644 index 000000000000..76a3caae13ca --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterPropertyPathTests.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.beanvalidation; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.validation.FieldError; +import org.springframework.validation.method.MethodValidationResult; +import org.springframework.validation.method.ParameterErrors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test method validation scenarios with cascaded violations on different types + * of method parameters and return values. + * + * @author Rossen Stoyanchev + */ +class MethodValidationAdapterPropertyPathTests { + + private static final Person validPerson = new Person("John"); + + private static final Person invalidPerson = new Person("Long John Silver"); + + private static final Class[] HINTS = new Class[0]; + + + private final MethodValidationAdapter validationAdapter = new MethodValidationAdapter(); + + + @Nested + class ArgumentTests { + + @Test + void fieldOfObjectPropertyOfBean() { + Method method = getMethod("addCourse"); + Object[] args = {new Course("CS 101", invalidPerson, Collections.emptyList())}; + + MethodValidationResult result = + validationAdapter.validateArguments(new MyService(), method, null, args, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + assertSingleFieldError(errors, 1, null, null, null, "professor.name", invalidPerson.name()); + } + + @Test + void fieldOfObjectPropertyOfListElement() { + Method method = getMethod("addCourseList"); + List courses = List.of(new Course("CS 101", invalidPerson, Collections.emptyList())); + + MethodValidationResult result = validationAdapter.validateArguments( + new MyService(), method, null, new Object[] {courses}, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + assertSingleFieldError(errors, 1, courses, 0, null, "professor.name", invalidPerson.name()); + } + + @Test + void fieldOfObjectPropertyOfListElements() { + Method method = getMethod("addCourseList"); + List courses = List.of( + new Course("CS 101", invalidPerson, Collections.emptyList()), + new Course("CS 102", invalidPerson, Collections.emptyList())); + + MethodValidationResult result = validationAdapter.validateArguments( + new MyService(), method, null, new Object[] {courses}, HINTS); + + assertThat(result.getAllErrors()).hasSize(2); + for (int i = 0; i < 2; i++) { + ParameterErrors errors = result.getBeanResults().get(i); + assertThat(errors.getContainerIndex()).isEqualTo(i); + assertThat(errors.getFieldError().getField()).isEqualTo("professor.name"); + } + + } + + @Test + void fieldOfObjectPropertyUnderListPropertyOfListElement() { + Method method = getMethod("addCourseList"); + Course cs101 = new Course("CS 101", invalidPerson, Collections.emptyList()); + Course cs201 = new Course("CS 201", validPerson, List.of(cs101)); + Course cs301 = new Course("CS 301", validPerson, List.of(cs201)); + List courses = List.of(cs301); + Object[] args = {courses}; + + MethodValidationResult result = + validationAdapter.validateArguments(new MyService(), method, null, args, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + + assertSingleFieldError(errors, 1, courses, 0, null, + "requiredCourses[0].requiredCourses[0].professor.name", invalidPerson.name()); + } + + @Test + void fieldOfObjectPropertyOfArrayElement() { + Method method = getMethod("addCourseArray"); + Course[] courses = new Course[] {new Course("CS 101", invalidPerson, Collections.emptyList())}; + + MethodValidationResult result = validationAdapter.validateArguments( + new MyService(), method, null, new Object[] {courses}, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + assertSingleFieldError(errors, 1, courses, 0, null, "professor.name", invalidPerson.name()); + } + + @Test + void fieldOfObjectPropertyOfMapValue() { + Method method = getMethod("addCourseMap"); + Map courses = Map.of("CS 101", new Course("CS 101", invalidPerson, Collections.emptyList())); + + MethodValidationResult result = validationAdapter.validateArguments( + new MyService(), method, null, new Object[] {courses}, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + assertSingleFieldError(errors, 1, courses, null, "CS 101", "professor.name", invalidPerson.name()); + } + + @Test + void fieldOfObjectPropertyOfOptionalBean() { + Method method = getMethod("addOptionalCourse"); + Optional optional = Optional.of(new Course("CS 101", invalidPerson, Collections.emptyList())); + Object[] args = {optional}; + + MethodValidationResult result = + validationAdapter.validateArguments(new MyService(), method, null, args, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + assertSingleFieldError(errors, 1, optional, null, null, "professor.name", invalidPerson.name()); + } + + } + + + @Nested + class ReturnValueTests { + + @Test + void fieldOfObjectPropertyOfBean() { + Method method = getMethod("getCourse"); + Course course = new Course("CS 101", invalidPerson, Collections.emptyList()); + + MethodValidationResult result = + validationAdapter.validateReturnValue(new MyService(), method, null, course, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + assertSingleFieldError(errors, 1, null, null, null, "professor.name", invalidPerson.name()); + } + + @Test + void fieldOfObjectPropertyOfListElement() { + Method method = getMethod("addCourseList"); + List courses = List.of(new Course("CS 101", invalidPerson, Collections.emptyList())); + + MethodValidationResult result = validationAdapter.validateArguments( + new MyService(), method, null, new Object[] {courses}, HINTS); + + assertThat(result.getAllErrors()).hasSize(1); + ParameterErrors errors = result.getBeanResults().get(0); + assertSingleFieldError(errors, 1, courses, 0, null, "professor.name", invalidPerson.name()); + } + + } + + + private void assertSingleFieldError( + ParameterErrors errors, int errorCount, + @Nullable Object container, @Nullable Integer index, @Nullable Object key, + String field, Object rejectedValue) { + + assertThat(errors.getErrorCount()).isEqualTo(errorCount); + assertThat(errors.getErrorCount()).isEqualTo(1); + assertThat(errors.getContainer()).isEqualTo(container); + assertThat(errors.getContainerIndex()).isEqualTo(index); + assertThat(errors.getContainerKey()).isEqualTo(key); + + FieldError fieldError = errors.getFieldError(); + assertThat(fieldError).isNotNull(); + assertThat(fieldError.getField()).isEqualTo(field); + assertThat(fieldError.getRejectedValue()).isEqualTo(rejectedValue); + } + + + private static Method getMethod(String methodName) { + return ClassUtils.getMethod(MyService.class, methodName, (Class[]) null); + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private static class MyService { + + public void addCourse(@Valid Course course) { + } + + public void addCourseList(@Valid List courses) { + } + + public void addCourseArray(@Valid Course[] courses) { + } + + public void addCourseMap(@Valid Map courses) { + } + + public void addOptionalCourse(@Valid Optional course) { + } + + @Valid + public Course getCourse(Course course) { + throw new UnsupportedOperationException(); + } + + @Valid + public List getCourseList() { + throw new UnsupportedOperationException(); + } + + } + + + private record Course(@NotBlank String title, @Valid Person professor, @Valid List requiredCourses) { + } + + + @SuppressWarnings("unused") + private record Person(@Size(min = 1, max = 5) String name) { + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java new file mode 100644 index 000000000000..3519210f7ea9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java @@ -0,0 +1,298 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.beanvalidation; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Consumer; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.MessageSourceResolvable; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.validation.FieldError; +import org.springframework.validation.method.MethodValidationResult; +import org.springframework.validation.method.ParameterErrors; +import org.springframework.validation.method.ParameterValidationResult; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MethodValidationAdapter}. + * + * @author Rossen Stoyanchev + * @author Sam Brannen + */ +class MethodValidationAdapterTests { + + private static final Person faustino1234 = new Person("Faustino1234", List.of("Working on Spring")); + + private static final Person cayetana6789 = new Person("Cayetana6789", List.of(" ")); + + + private final MethodValidationAdapter validationAdapter = new MethodValidationAdapter(); + + private final Locale originalLocale = Locale.getDefault(); + + + @BeforeEach + void setDefaultLocaleToEnglish() { + Locale.setDefault(Locale.ENGLISH); + } + + @AfterEach + void resetDefaultLocale() { + Locale.setDefault(this.originalLocale); + } + + @Test + void validateArguments() { + MyService target = new MyService(); + Method method = getMethod(target, "addStudent"); + + testArgs(target, method, new Object[] {faustino1234, cayetana6789, 3}, ex -> { + + assertThat(ex.getAllValidationResults()).hasSize(3); + + assertBeanResult(ex.getBeanResults().get(0), 0, "student", faustino1234, List.of(""" + Field error in object 'student' on field 'name': rejected value [Faustino1234]; \ + codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [student.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]""")); + + assertBeanResult(ex.getBeanResults().get(1), 1, "guardian", cayetana6789, List.of(""" + Field error in object 'guardian' on field 'name': rejected value [Cayetana6789]; \ + codes [Size.guardian.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [guardian.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]""", """ + Field error in object 'guardian' on field 'hobbies[0]': rejected value [ ]; \ + codes [NotBlank.guardian.hobbies[0],NotBlank.guardian.hobbies,NotBlank.hobbies[0],\ + NotBlank.hobbies,NotBlank.java.lang.String,NotBlank]; arguments \ + [org.springframework.context.support.DefaultMessageSourceResolvable: codes \ + [guardian.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \ + default message [must not be blank]""")); + + assertValueResult(ex.getValueResults().get(0), 2, 3, List.of(""" + org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [Max.myService#addStudent.degrees,Max.degrees,Max.int,Max]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [myService#addStudent.degrees,degrees]; arguments []; default message [degrees],2]; \ + default message [must be less than or equal to 2]""")); + }); + } + + @Test + void validateArgumentWithCustomObjectName() { + MyService target = new MyService(); + Method method = getMethod(target, "addStudent"); + + this.validationAdapter.setObjectNameResolver((param, value) -> "studentToAdd"); + + testArgs(target, method, new Object[] {faustino1234, new Person("Joe", List.of()), 1}, ex -> { + + assertThat(ex.getAllValidationResults()).hasSize(1); + + assertBeanResult(ex.getBeanResults().get(0), 0, "studentToAdd", faustino1234, List.of(""" + Field error in object 'studentToAdd' on field 'name': rejected value [Faustino1234]; \ + codes [Size.studentToAdd.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [studentToAdd.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]""")); + }); + } + + @Test + void validateReturnValue() { + MyService target = new MyService(); + + testReturnValue(target, getMethod(target, "getIntValue"), 4, ex -> { + + assertThat(ex.getAllValidationResults()).hasSize(1); + + assertValueResult(ex.getValueResults().get(0), -1, 4, List.of(""" + org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [Min.myService#getIntValue,Min,Min.int]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [myService#getIntValue]; arguments []; default message [],5]; \ + default message [must be greater than or equal to 5]""")); + }); + } + + @Test + void validateReturnValueBean() { + MyService target = new MyService(); + + testReturnValue(target, getMethod(target, "getPerson"), faustino1234, ex -> { + + assertThat(ex.getAllValidationResults()).hasSize(1); + + assertBeanResult(ex.getBeanResults().get(0), -1, "person", faustino1234, List.of(""" + Field error in object 'person' on field 'name': rejected value [Faustino1234]; \ + codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [person.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]""")); + }); + } + + @Test + void validateBeanListArgument() { + MyService target = new MyService(); + Method method = getMethod(target, "addPeople"); + + testArgs(target, method, new Object[] {List.of(faustino1234, cayetana6789)}, ex -> { + + assertThat(ex.getAllValidationResults()).hasSize(2); + + int paramIndex = 0; + String objectName = "people"; + List results = ex.getBeanResults(); + + assertBeanResult(results.get(0), paramIndex, objectName, faustino1234, List.of(""" + Field error in object 'people' on field 'name': rejected value [Faustino1234]; \ + codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [people.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]""")); + + assertBeanResult(results.get(1), paramIndex, objectName, cayetana6789, List.of(""" + Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \ + codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [people.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]""", """ + Field error in object 'people' on field 'hobbies[0]': rejected value [ ]; \ + codes [NotBlank.people.hobbies[0],NotBlank.people.hobbies,NotBlank.hobbies[0],\ + NotBlank.hobbies,NotBlank.java.lang.String,NotBlank]; arguments \ + [org.springframework.context.support.DefaultMessageSourceResolvable: codes \ + [people.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \ + default message [must not be blank]""")); + }); + } + + @Test + void validateValueListArgument() { + MyService target = new MyService(); + Method method = getMethod(target, "addHobbies"); + + testArgs(target, method, new Object[] {List.of(" ")}, ex -> { + assertThat(ex.getAllValidationResults()).hasSize(1); + assertValueResult(ex.getValueResults().get(0), 0, " ", List.of(""" + org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [NotBlank.myService#addHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.List,NotBlank]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [myService#addHobbies.hobbies,hobbies]; \ + arguments []; default message [hobbies]]; default message [must not be blank]""")); + }); + } + + @Test // gh-33150 + void validateValueSetArgument() { + MyService target = new MyService(); + Method method = getMethod(target, "addUniqueHobbies"); + + testArgs(target, method, new Object[] {Set.of("test", " ")}, ex -> { + assertThat(ex.getAllValidationResults()).hasSize(1); + assertValueResult(ex.getValueResults().get(0), 0, Set.of("test", " "), List.of(""" + org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [NotBlank.myService#addUniqueHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.Set,NotBlank]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [myService#addUniqueHobbies.hobbies,hobbies]; \ + arguments []; default message [hobbies]]; default message [must not be blank]""")); + }); + } + + private void testArgs(Object target, Method method, Object[] args, Consumer consumer) { + consumer.accept(this.validationAdapter.validateArguments(target, method, null, args, new Class[0])); + } + + private void testReturnValue(Object target, Method method, @Nullable Object value, Consumer consumer) { + consumer.accept(this.validationAdapter.validateReturnValue(target, method, null, value, new Class[0])); + } + + private static void assertBeanResult( + ParameterErrors errors, int parameterIndex, String objectName, @Nullable Object argument, + List fieldErrors) { + + assertThat(errors.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex); + assertThat(errors.getObjectName()).isEqualTo(objectName); + assertThat(errors.getArgument()).isSameAs(argument); + + assertThat(errors.getFieldErrors()) + .extracting(FieldError::toString) + .containsExactlyInAnyOrderElementsOf(fieldErrors); + } + + private static void assertValueResult( + ParameterValidationResult result, int parameterIndex, Object argument, List errors) { + + assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex); + assertThat(result.getArgument()).isEqualTo(argument); + assertThat(result.getResolvableErrors()) + .extracting(MessageSourceResolvable::toString) + .containsExactlyInAnyOrderElementsOf(errors); + } + + private static Method getMethod(Object target, String methodName) { + return ClassUtils.getMethod(target.getClass(), methodName, (Class[]) null); + } + + + @SuppressWarnings("unused") + private static class MyService { + + public void addStudent(@Valid Person student, @Valid Person guardian, @Max(2) int degrees) { + } + + @Min(5) + public int getIntValue() { + throw new UnsupportedOperationException(); + } + + @Valid + public Person getPerson() { + throw new UnsupportedOperationException(); + } + + public void addPeople(@Valid List people) { + } + + public void addHobbies(List<@NotBlank String> hobbies) { + } + + public void addUniqueHobbies(Set<@NotBlank String> hobbies) { + } + } + + + @SuppressWarnings("unused") + private record Person(@Size(min = 1, max = 10) String name, List<@NotBlank String> hobbies) { + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyReactorTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyReactorTests.java new file mode 100644 index 000000000000..b201a2cca607 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyReactorTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.beanvalidation; + +import java.util.Locale; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.Size; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.validation.method.MethodValidationException; +import org.springframework.validation.method.ParameterErrors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for method validation proxy with reactor. + * + * @author Rossen Stoyanchev + */ +class MethodValidationProxyReactorTests { + + @Test + void validMonoArgument() { + MyService myService = initProxy(new MyService(), false); + Mono personMono = Mono.just(new Person("Faustino1234")); + + StepVerifier.create(myService.addPerson(personMono)) + .expectErrorSatisfies(t -> { + ConstraintViolationException ex = (ConstraintViolationException) t; + Set> violations = ex.getConstraintViolations(); + assertThat(violations).hasSize(1); + assertThat(violations.iterator().next().getMessage()).isEqualTo("size must be between 1 and 10"); + }) + .verify(); + } + + @Test + void validFluxArgument() { + MyService myService = initProxy(new MyService(), false); + Flux personFlux = Flux.just(new Person("Faust"), new Person("Faustino1234")); + + StepVerifier.create(myService.addPersons(personFlux)) + .expectErrorSatisfies(t -> { + ConstraintViolationException ex = (ConstraintViolationException) t; + Set> violations = ex.getConstraintViolations(); + assertThat(violations).hasSize(1); + assertThat(violations.iterator().next().getMessage()).isEqualTo("size must be between 1 and 10"); + }) + .verify(); + } + + @Test + void validMonoArgumentWithAdaptedViolations() { + MyService myService = initProxy(new MyService(), true); + Mono personMono = Mono.just(new Person("Faustino1234")); + + StepVerifier.create(myService.addPerson(personMono)) + .expectErrorSatisfies(t -> { + MethodValidationException ex = (MethodValidationException) t; + assertThat(ex.getAllValidationResults()).hasSize(1); + + ParameterErrors errors = ex.getBeanResults().get(0); + assertThat(errors.getErrorCount()).isEqualTo(1); + assertThat(errors.getFieldErrors().get(0).toString()).isEqualTo(""" + Field error in object 'Person' on field 'name': rejected value [Faustino1234]; \ + codes [Size.Person.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [Person.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]"""); + }) + .verify(); + } + + private static MyService initProxy(Object target, boolean adaptViolations) { + Locale oldDefault = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + MethodValidationInterceptor interceptor = new MethodValidationInterceptor(() -> validator, adaptViolations); + ProxyFactory factory = new ProxyFactory(target); + factory.addAdvice(interceptor); + return (MyService) factory.getProxy(); + } + finally { + Locale.setDefault(oldDefault); + } + } + + + @SuppressWarnings("unused") + static class MyService { + + public Mono addPerson(@Valid Mono personMono) { + return personMono.then(); + } + + public Mono addPersons(@Valid Flux personFlux) { + return personFlux.then(); + } + } + + + @SuppressWarnings("unused") + record Person(@Size(min = 1, max = 10) String name) { + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java similarity index 53% rename from spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java rename to spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java index db7a9b0d05cc..90224491f438 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; -import jakarta.validation.ValidationException; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotNull; @@ -28,10 +29,13 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -45,91 +49,114 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.validation.annotation.Validated; +import org.springframework.validation.method.MethodValidationException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** + * Tests for proxy-based method validation via {@link MethodValidationInterceptor} + * and/or {@link MethodValidationPostProcessor}. + * * @author Juergen Hoeller + * @author Rossen Stoyanchev */ -public class MethodValidationTests { +class MethodValidationProxyTests { - @Test + @ParameterizedTest + @ValueSource(booleans = {true, false}) @SuppressWarnings("unchecked") - public void testMethodValidationInterceptor() { + void testMethodValidationInterceptor(boolean adaptViolations) { MyValidBean bean = new MyValidBean(); - ProxyFactory proxyFactory = new ProxyFactory(bean); - proxyFactory.addAdvice(new MethodValidationInterceptor()); - proxyFactory.addAdvisor(new AsyncAnnotationAdvisor()); - doTestProxyValidation((MyValidInterface) proxyFactory.getProxy()); + ProxyFactory factory = new ProxyFactory(bean); + factory.addAdvice(adaptViolations ? + new MethodValidationInterceptor(() -> Validation.buildDefaultValidatorFactory().getValidator(), true) : + new MethodValidationInterceptor()); + factory.addAdvisor(new AsyncAnnotationAdvisor()); + doTestProxyValidation((MyValidInterface) factory.getProxy(), + (adaptViolations ? MethodValidationException.class : ConstraintViolationException.class)); } - @Test + @ParameterizedTest + @ValueSource(booleans = {true, false}) @SuppressWarnings("unchecked") - public void testMethodValidationPostProcessor() { - StaticApplicationContext ac = new StaticApplicationContext(); - ac.registerSingleton("mvpp", MethodValidationPostProcessor.class); + void testMethodValidationPostProcessor(boolean adaptViolations) { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerBean(MethodValidationPostProcessor.class, adaptViolations ? + () -> { + MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); + postProcessor.setAdaptConstraintViolations(true); + return postProcessor; + } : + MethodValidationPostProcessor::new); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("beforeExistingAdvisors", false); - ac.registerSingleton("aapp", AsyncAnnotationBeanPostProcessor.class, pvs); - ac.registerSingleton("bean", MyValidBean.class); - ac.refresh(); - doTestProxyValidation(ac.getBean("bean", MyValidInterface.class)); - ac.close(); + context.registerSingleton("aapp", AsyncAnnotationBeanPostProcessor.class, pvs); + context.registerSingleton("bean", MyValidBean.class); + context.refresh(); + doTestProxyValidation(context.getBean("bean", MyValidInterface.class), + adaptViolations ? MethodValidationException.class : ConstraintViolationException.class); + context.close(); } - @Test // gh-29782 - @SuppressWarnings("unchecked") - public void testMethodValidationPostProcessorForInterfaceOnlyProxy() { + @Test // gh-29782 + void testMethodValidationPostProcessorForInterfaceOnlyProxy() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MethodValidationPostProcessor.class); context.registerBean(MyValidInterface.class, () -> ProxyFactory.getProxy(MyValidInterface.class, new MyValidClientInterfaceMethodInterceptor())); context.refresh(); - doTestProxyValidation(context.getBean(MyValidInterface.class)); + doTestProxyValidation(context.getBean(MyValidInterface.class), ConstraintViolationException.class); context.close(); } - private void doTestProxyValidation(MyValidInterface proxy) { + @SuppressWarnings("DataFlowIssue") + private void doTestProxyValidation(MyValidInterface proxy, Class expectedExceptionClass) { assertThat(proxy.myValidMethod("value", 5)).isNotNull(); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidMethod("value", 15)); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidMethod(null, 5)); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidMethod("value", 0)); + assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidMethod("value", 15)); + assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidMethod(null, 5)); + assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidMethod("value", 0)); proxy.myValidAsyncMethod("value", 5); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidAsyncMethod("value", 15)); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myValidAsyncMethod(null, 5)); + assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidAsyncMethod("value", 15)); + assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidAsyncMethod(null, 5)); assertThat(proxy.myGenericMethod("myValue")).isEqualTo("myValue"); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> - proxy.myGenericMethod(null)); + assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myGenericMethod(null)); } @Test - public void testLazyValidatorForMethodValidation() { - @SuppressWarnings("resource") - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( - LazyMethodValidationConfig.class, CustomValidatorBean.class, - MyValidBean.class, MyValidFactoryBean.class); - ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); + void testLazyValidatorForMethodValidation() { + doTestLazyValidatorForMethodValidation(LazyMethodValidationConfig.class); } @Test - public void testLazyValidatorForMethodValidationWithProxyTargetClass() { - @SuppressWarnings("resource") - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( - LazyMethodValidationConfigWithProxyTargetClass.class, CustomValidatorBean.class, - MyValidBean.class, MyValidFactoryBean.class); - ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); + void testLazyValidatorForMethodValidationWithProxyTargetClass() { + doTestLazyValidatorForMethodValidation(LazyMethodValidationConfigWithProxyTargetClass.class); + } + + @Test + void testLazyValidatorForMethodValidationWithValidatorProvider() { + doTestLazyValidatorForMethodValidation(LazyMethodValidationConfigWithValidatorProvider.class); + } + + private void doTestLazyValidatorForMethodValidation(Class configClass) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(configClass, CustomValidatorBean.class, MyValidBean.class, MyValidFactoryBean.class); + context.getDefaultListableBeanFactory().getBeanDefinition("customValidatorBean").setLazyInit(true); + context.refresh(); + + assertThat(context.getDefaultListableBeanFactory().containsSingleton("customValidatorBean")).isFalse(); + context.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); + assertThat(context.getDefaultListableBeanFactory().containsSingleton("customValidatorBean")).isTrue(); + + context.close(); } @MyStereotype public static class MyValidBean implements MyValidInterface { + @SuppressWarnings("DataFlowIssue") + @NotNull @Override public Object myValidMethod(String arg1, int arg2) { return (arg2 == 0 ? null : "value"); @@ -159,6 +186,8 @@ public Class getObjectType() { return String.class; } + @SuppressWarnings("DataFlowIssue") + @NotNull @Override public Object myValidMethod(String arg1, int arg2) { return (arg2 == 0 ? null : "value"); @@ -178,10 +207,12 @@ public String myGenericMethod(String value) { @MyStereotype public interface MyValidInterface { - @NotNull Object myValidMethod(@NotNull(groups = MyGroup.class) String arg1, @Max(10) int arg2); + @NotNull + Object myValidMethod(@NotNull(groups = MyGroup.class) String arg1, @Max(10) int arg2); @MyValid - @Async void myValidAsyncMethod(@NotNull(groups = OtherGroup.class) String arg1, @Max(10) int arg2); + @Async + void myValidAsyncMethod(@NotNull(groups = OtherGroup.class) String arg1, @Max(10) int arg2); T myGenericMethod(@NotNull T value); } @@ -193,7 +224,7 @@ static class MyValidClientInterfaceMethodInterceptor implements MethodIntercepto @Nullable @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public Object invoke(MethodInvocation invocation) { Method method; try { method = ClassUtils.getMethod(MyValidBean.class, invocation.getMethod().getName(), (Class[]) null); @@ -251,4 +282,16 @@ public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy } } + + @Configuration + public static class LazyMethodValidationConfigWithValidatorProvider { + + @Bean + public static MethodValidationPostProcessor methodValidationPostProcessor(ObjectProvider validator) { + MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); + postProcessor.setValidatorProvider(validator); + return postProcessor; + } + } + } diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java index 060869a17f0b..47801e12d9f2 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ * @author Kazuki Shimizu * @author Juergen Hoeller */ -public class SpringValidatorAdapterTests { +class SpringValidatorAdapterTests { private final Validator nativeValidator = Validation.buildDefaultValidatorFactory().getValidator(); @@ -71,7 +71,7 @@ public class SpringValidatorAdapterTests { @BeforeEach - public void setupSpringValidatorAdapter() { + void setupSpringValidatorAdapter() { messageSource.addMessage("Size", Locale.ENGLISH, "Size of {0} must be between {2} and {1}"); messageSource.addMessage("Same", Locale.ENGLISH, "{2} must be same value as {1}"); messageSource.addMessage("password", Locale.ENGLISH, "Password"); @@ -80,7 +80,7 @@ public void setupSpringValidatorAdapter() { @Test - public void testUnwrap() { + void testUnwrap() { Validator nativeValidator = validatorAdapter.unwrap(Validator.class); assertThat(nativeValidator).isSameAs(this.nativeValidator); } @@ -174,7 +174,7 @@ public void testApplyMessageSourceResolvableToStringArgumentValueWithAlwaysUseMe } @Test - public void testPatternMessage() { + void testPatternMessage() { TestBean testBean = new TestBean(); testBean.setEmail("X"); testBean.setConfirmEmail("X"); diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java index 37b867ca2fdb..660d9afd616b 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -159,19 +159,19 @@ void springValidation() { assertThat(fieldError.getField()).isEqualTo("name"); List errorCodes = Arrays.asList(fieldError.getCodes()); assertThat(errorCodes).hasSize(4); - assertThat(errorCodes.contains("NotNull.person.name")).isTrue(); - assertThat(errorCodes.contains("NotNull.name")).isTrue(); - assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); - assertThat(errorCodes.contains("NotNull")).isTrue(); + assertThat(errorCodes).contains("NotNull.person.name"); + assertThat(errorCodes).contains("NotNull.name"); + assertThat(errorCodes).contains("NotNull.java.lang.String"); + assertThat(errorCodes).contains("NotNull"); fieldError = result.getFieldError("address.street"); assertThat(fieldError.getField()).isEqualTo("address.street"); errorCodes = Arrays.asList(fieldError.getCodes()); assertThat(errorCodes).hasSize(5); - assertThat(errorCodes.contains("NotNull.person.address.street")).isTrue(); - assertThat(errorCodes.contains("NotNull.address.street")).isTrue(); - assertThat(errorCodes.contains("NotNull.street")).isTrue(); - assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); - assertThat(errorCodes.contains("NotNull")).isTrue(); + assertThat(errorCodes).contains("NotNull.person.address.street"); + assertThat(errorCodes).contains("NotNull.address.street"); + assertThat(errorCodes).contains("NotNull.street"); + assertThat(errorCodes).contains("NotNull.java.lang.String"); + assertThat(errorCodes).contains("NotNull"); validator.destroy(); } @@ -191,8 +191,8 @@ void springValidationWithClassLevel() { ObjectError globalError = result.getGlobalError(); List errorCodes = Arrays.asList(globalError.getCodes()); assertThat(errorCodes).hasSize(2); - assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); - assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + assertThat(errorCodes).contains("NameAddressValid.person"); + assertThat(errorCodes).contains("NameAddressValid"); validator.destroy(); } @@ -213,8 +213,8 @@ void springValidationWithAutowiredValidator() { ObjectError globalError = result.getGlobalError(); List errorCodes = Arrays.asList(globalError.getCodes()); assertThat(errorCodes).hasSize(2); - assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); - assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + assertThat(errorCodes).contains("NameAddressValid.person"); + assertThat(errorCodes).contains("NameAddressValid"); validator.destroy(); ctx.close(); diff --git a/spring-context/src/test/kotlin/org/springframework/cache/KotlinCacheReproTests.kt b/spring-context/src/test/kotlin/org/springframework/cache/KotlinCacheReproTests.kt new file mode 100644 index 000000000000..26e6b1e20aee --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/cache/KotlinCacheReproTests.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache + +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.testfixture.beans.TestBean +import org.springframework.cache.CacheReproTests.* +import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.CachePut +import org.springframework.cache.annotation.Cacheable +import org.springframework.cache.annotation.EnableCaching +import org.springframework.cache.concurrent.ConcurrentMapCacheManager +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +class KotlinCacheReproTests { + + @Test + fun spr14235AdaptsToSuspendingFunction() { + runBlocking { + val context = AnnotationConfigApplicationContext( + Spr14235Config::class.java, + Spr14235SuspendingService::class.java + ) + val bean = context.getBean(Spr14235SuspendingService::class.java) + val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!! + val tb: TestBean = bean.findById("tb1") + assertThat(bean.findById("tb1")).isSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb) + bean.clear() + val tb2: TestBean = bean.findById("tb1") + assertThat(tb2).isNotSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb2) + bean.clear() + bean.insertItem(tb) + assertThat(bean.findById("tb1")).isSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb) + context.close() + } + } + + @Test + fun spr14235AdaptsToSuspendingFunctionWithSync() { + runBlocking { + val context = AnnotationConfigApplicationContext( + Spr14235Config::class.java, + Spr14235SuspendingServiceSync::class.java + ) + val bean = context.getBean(Spr14235SuspendingServiceSync::class.java) + val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!! + val tb = bean.findById("tb1") + assertThat(bean.findById("tb1")).isSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb) + cache.clear() + val tb2 = bean.findById("tb1") + assertThat(tb2).isNotSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb2) + cache.clear() + bean.insertItem(tb) + assertThat(bean.findById("tb1")).isSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb) + context.close() + } + } + + @Test + fun spr15271FindsOnInterfaceWithInterfaceProxy() { + val context = AnnotationConfigApplicationContext(Spr15271ConfigA::class.java) + val bean = context.getBean(Spr15271Interface::class.java) + val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!! + val tb = TestBean("tb1") + bean.insertItem(tb) + assertThat(bean.findById("tb1").get()).isSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb) + context.close() + } + + @Test + fun spr15271FindsOnInterfaceWithCglibProxy() { + val context = AnnotationConfigApplicationContext(Spr15271ConfigB::class.java) + val bean = context.getBean(Spr15271Interface::class.java) + val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!! + val tb = TestBean("tb1") + bean.insertItem(tb) + assertThat(bean.findById("tb1").get()).isSameAs(tb) + assertThat(cache["tb1"]!!.get()).isSameAs(tb) + context.close() + } + + + open class Spr14235SuspendingService { + + @Cacheable(value = ["itemCache"]) + open suspend fun findById(id: String): TestBean { + return TestBean(id) + } + + @CachePut(cacheNames = ["itemCache"], key = "#item.name") + open suspend fun insertItem(item: TestBean): TestBean { + return item + } + + @CacheEvict(cacheNames = ["itemCache"], allEntries = true) + open suspend fun clear() { + } + } + + + open class Spr14235SuspendingServiceSync { + @Cacheable(value = ["itemCache"], sync = true) + open suspend fun findById(id: String): TestBean { + return TestBean(id) + } + + @CachePut(cacheNames = ["itemCache"], key = "#item.name") + open suspend fun insertItem(item: TestBean): TestBean { + return item + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + class Spr14235Config { + @Bean + fun cacheManager(): CacheManager { + return ConcurrentMapCacheManager() + } + } + +} diff --git a/spring-context/src/test/kotlin/org/springframework/cache/interceptor/KotlinSimpleKeyGeneratorTests.kt b/spring-context/src/test/kotlin/org/springframework/cache/interceptor/KotlinSimpleKeyGeneratorTests.kt new file mode 100644 index 000000000000..c8afdbe75e28 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/cache/interceptor/KotlinSimpleKeyGeneratorTests.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache.interceptor + +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.util.ReflectionUtils +import kotlin.coroutines.Continuation + +/** + * Tests for [SimpleKeyGenerator] and [SimpleKey]. + * + * @author Sebastien Deleuze + */ +class KotlinSimpleKeyGeneratorTests { + + private val generator = SimpleKeyGenerator() + + @Test + fun ignoreContinuationArgumentWithNoParameter() { + val method = ReflectionUtils.findMethod(javaClass, "suspendingMethod", Continuation::class.java)!! + val continuation = mockk>() + val key = generator.generate(this, method, continuation) + assertThat(key).isEqualTo(SimpleKey.EMPTY) + } + + @Test + fun ignoreContinuationArgumentWithOneParameter() { + val method = ReflectionUtils.findMethod(javaClass, "suspendingMethod", String::class.java, Continuation::class.java)!! + val continuation = mockk>() + val key = generator.generate(this, method, "arg", continuation) + assertThat(key).isEqualTo("arg") + } + + @Test + fun ignoreContinuationArgumentWithMultipleParameters() { + val method = ReflectionUtils.findMethod(javaClass, "suspendingMethod", String::class.java, String::class.java, Continuation::class.java)!! + val continuation = mockk>() + val key = generator.generate(this, method, "arg1", "arg2", continuation) + assertThat(key).isEqualTo(SimpleKey("arg1", "arg2")) + } + + + @Suppress("unused", "RedundantSuspendModifier") + suspend fun suspendingMethod() { + } + + @Suppress("unused", "UNUSED_PARAMETER", "RedundantSuspendModifier") + suspend fun suspendingMethod(param: String) { + } + + @Suppress("unused", "UNUSED_PARAMETER", "RedundantSuspendModifier") + suspend fun suspendingMethod(param1: String, param2: String) { + } + +} diff --git a/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt index fbbb48eccd5d..4252607b5868 100644 --- a/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt +++ b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,17 @@ class KotlinReflectionBeanRegistrationAotProcessorTests { assertThat(generationContext.runtimeHints.reflection().typeHints()).isEmpty() } + @Test + fun shouldGenerateOuterClassHints() { + process(OuterBean.NestedBean::class.java) + assertThat( + RuntimeHintsPredicates.reflection() + .onType(OuterBean.NestedBean::class.java) + .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS) + .and(RuntimeHintsPredicates.reflection().onType(OuterBean::class.java)) + ).accepts(generationContext.runtimeHints) + } + private fun process(beanClass: Class<*>) { createContribution(beanClass)?.applyTo(generationContext, Mockito.mock(BeanRegistrationCode::class.java)) } @@ -87,4 +98,8 @@ class KotlinReflectionBeanRegistrationAotProcessorTests { } } + class OuterBean { + class NestedBean + } + } diff --git a/spring-context/src/test/kotlin/org/springframework/context/event/KotlinApplicationListenerMethodAdapterTests.kt b/spring-context/src/test/kotlin/org/springframework/context/event/KotlinApplicationListenerMethodAdapterTests.kt new file mode 100644 index 000000000000..4ac69bb01581 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/event/KotlinApplicationListenerMethodAdapterTests.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.context.event + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.context.ApplicationEvent +import org.springframework.core.ResolvableType +import org.springframework.util.ReflectionUtils +import java.lang.reflect.Method +import kotlin.coroutines.Continuation + +/** + * Kotlin tests for [ApplicationListenerMethodAdapter]. + * + * @author Sebastien Deleuze + */ +class KotlinApplicationListenerMethodAdapterTests { + + private val sampleEvents = Mockito.spy(SampleEvents()) + + @Test + fun rawListener() { + val method = ReflectionUtils.findMethod(SampleEvents::class.java, "handleRaw", ApplicationEvent::class.java, Continuation::class.java)!! + supportsEventType(true, method, ResolvableType.forClass(ApplicationEvent::class.java)) + } + + @Test + fun listenerWithMoreThanOneParameter() { + val method = ReflectionUtils.findMethod(SampleEvents::class.java, "moreThanOneParameter", + String::class.java, Int::class.java, Continuation::class.java)!! + Assertions.assertThatIllegalStateException().isThrownBy { + createTestInstance( + method + ) + } + } + + private fun supportsEventType(match: Boolean, method: Method, eventType: ResolvableType) { + val adapter: ApplicationListenerMethodAdapter = createTestInstance(method) + Assertions.assertThat(adapter.supportsEventType(eventType)) + .`as`("Wrong match for event '$eventType' on $method").isEqualTo(match) + } + + private fun createTestInstance(method: Method): ApplicationListenerMethodAdapter { + return StaticApplicationListenerMethodAdapter(method, this.sampleEvents) + } + + + private class StaticApplicationListenerMethodAdapter(method: Method, private val targetBean: Any) : + ApplicationListenerMethodAdapter("unused", targetBean.javaClass, method) { + public override fun getTargetBean(): Any { + return targetBean + } + } + + @Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER") + private class SampleEvents { + + @EventListener + suspend fun handleRaw(event: ApplicationEvent) { + } + + @EventListener + suspend fun moreThanOneParameter(foo: String, bar: Int) { + } + } + +} diff --git a/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt index 5ac22c8d64f6..50e22b7012e3 100644 --- a/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt +++ b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,13 @@ package org.springframework.context.support import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.jupiter.api.fail import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail import org.springframework.beans.factory.NoSuchBeanDefinitionException import org.springframework.beans.factory.getBean +import org.springframework.beans.factory.getBeanProvider import org.springframework.context.support.BeanDefinitionDsl.* -import org.springframework.core.env.SimpleCommandLinePropertySource +import org.springframework.core.Ordered import org.springframework.core.env.get import org.springframework.core.testfixture.env.MockPropertySource import java.util.stream.Collectors @@ -90,7 +91,7 @@ class BeanDefinitionDslTests { } val context = GenericApplicationContext().apply { - environment.propertySources.addFirst(SimpleCommandLinePropertySource("--name=foofoo")) + environment.propertySources.addFirst(org.springframework.core.env.SimpleCommandLinePropertySource("--name=foofoo")) beans.initialize(this) refresh() } @@ -197,6 +198,25 @@ class BeanDefinitionDslTests { } catch (ignored: Exception) { } } + + @Test + fun `Declare beans with ordering`() { + val beans = beans { + bean(order = Ordered.LOWEST_PRECEDENCE) { + FooFoo("lowest") + } + bean(order = Ordered.HIGHEST_PRECEDENCE) { + FooFoo("highest") + } + } + + val context = GenericApplicationContext().apply { + beans.initialize(this) + refresh() + } + + assertThat(context.getBeanProvider().orderedStream().map { it.name }).containsExactly("highest", "lowest") + } } class Foo diff --git a/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorKotlinTests.kt b/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorKotlinTests.kt new file mode 100644 index 000000000000..86548ef0d994 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorKotlinTests.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.annotation + +import org.aopalliance.intercept.MethodInvocation +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.mockito.Mockito + +/** + * Kotlin tests for [AnnotationAsyncExecutionInterceptor]. + * + * @author Sebastien Deleuze + */ +class AnnotationAsyncExecutionInterceptorKotlinTests { + + @Test + fun nullableUnitReturnValue() { + val interceptor = AnnotationAsyncExecutionInterceptor(null) + + class C { @Async fun nullableUnit(): Unit? = null } + val invocation = Mockito.mock() + given(invocation.method).willReturn(C::class.java.getDeclaredMethod("nullableUnit")) + + Assertions.assertThat(interceptor.invoke(invocation)).isNull() + } + +} diff --git a/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/KotlinScheduledAnnotationReactiveSupportTests.kt b/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/KotlinScheduledAnnotationReactiveSupportTests.kt new file mode 100644 index 000000000000..b6452c1a4518 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/KotlinScheduledAnnotationReactiveSupportTests.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.annotation + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupport.getPublisherFor +import org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupport.isReactive +import org.springframework.util.ReflectionUtils +import reactor.core.publisher.Mono +import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.Continuation + +/** + * @author Simon Baslé + * @since 6.1 + */ +class KotlinScheduledAnnotationReactiveSupportTests { + + private var target: SuspendingFunctions? = SuspendingFunctions() + + + @Test + fun ensureReactor() { + assertThat(ScheduledAnnotationReactiveSupport.reactorPresent).isTrue + } + + @Test + fun ensureKotlinCoroutineReactorBridge() { + assertThat(ScheduledAnnotationReactiveSupport.coroutinesReactorPresent).isTrue + } + + @ParameterizedTest + @ValueSource(strings = ["suspending", "suspendingReturns"]) + fun isReactiveSuspending(methodName: String) { + val method = ReflectionUtils.findMethod(SuspendingFunctions::class.java, methodName, Continuation::class.java)!! + assertThat(isReactive(method)).isTrue + } + + @ParameterizedTest + @ValueSource(strings = ["flow", "deferred"]) + fun isReactiveKotlinType(methodName: String) { + val method = ReflectionUtils.findMethod(SuspendingFunctions::class.java, methodName)!! + assertThat(isReactive(method)).isTrue + } + + @Test + fun isNotReactive() { + val method = ReflectionUtils.findMethod(SuspendingFunctions::class.java, "notSuspending")!! + assertThat(isReactive(method)).isFalse + } + + @Test + fun checkKotlinRuntimeIfNeeded() { + val suspendingMethod = ReflectionUtils.findMethod(SuspendingFunctions::class.java, "suspending", Continuation::class.java)!! + val notSuspendingMethod = ReflectionUtils.findMethod(SuspendingFunctions::class.java, "notSuspending")!! + + assertThat(isReactive(suspendingMethod)).describedAs("suspending").isTrue() + assertThat(isReactive(notSuspendingMethod)).describedAs("not suspending").isFalse() + } + + @Test + fun isReactiveRejectsWithParams() { + val m = ReflectionUtils.findMethod(SuspendingFunctions::class.java, "withParam", String::class.java, Continuation::class.java)!! + + //isReactive rejects with some context + Assertions.assertThatIllegalArgumentException().isThrownBy { isReactive(m) } + .withMessage("Kotlin suspending functions may only be annotated with @Scheduled if declared without arguments") + .withNoCause() + } + + @Test + fun rejectNotSuspending() { + val m = ReflectionUtils.findMethod(SuspendingFunctions::class.java, "notSuspending") + + //static helper method + Assertions.assertThatIllegalArgumentException().isThrownBy { getPublisherFor(m!!, target!!) } + .withMessage("Cannot convert @Scheduled reactive method return type to Publisher") + .withNoCause() + } + + @Test + fun suspendingThrowIsTurnedToMonoError() { + val m = ReflectionUtils.findMethod(SuspendingFunctions::class.java, "throwsIllegalState", Continuation::class.java) + + val mono = Mono.from(getPublisherFor(m!!, target!!)) + + Assertions.assertThatIllegalStateException().isThrownBy { mono.block() } + .withMessage("expected") + .withNoCause() + } + + @Test + fun turningSuspendingFunctionToMonoDoesntExecuteTheMethod() { + val m = ReflectionUtils.findMethod(SuspendingFunctions::class.java, "suspendingTracking", Continuation::class.java) + val mono = Mono.from(getPublisherFor(m!!, target!!)) + + assertThat(target!!.subscription).hasValue(0) + mono.block() + assertThat(target!!.subscription).describedAs("after subscription").hasValue(1) + } + + + internal class SuspendingFunctions { + suspend fun suspending() { + } + + suspend fun suspendingReturns(): String = "suspended" + + suspend fun withParam(param: String): String { + return param + } + + suspend fun throwsIllegalState() { + throw IllegalStateException("expected") + } + + var subscription = AtomicInteger() + suspend fun suspendingTracking() { + subscription.incrementAndGet() + } + + fun notSuspending() { } + + fun flow(): Flow { + return flowOf() + } + + fun deferred(): Deferred { + return CompletableDeferred() + } + } + +} diff --git a/spring-context/src/test/resources/example/scannable/spring.components b/spring-context/src/test/resources/example/scannable/spring.components index a859835ba72e..3fdf592b1965 100644 --- a/spring-context/src/test/resources/example/scannable/spring.components +++ b/spring-context/src/test/resources/example/scannable/spring.components @@ -10,5 +10,9 @@ example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Compon example.scannable.sub.BarComponent=org.springframework.stereotype.Component example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean example.scannable.JakartaNamedComponent=jakarta.inject.Named +example.scannable.JavaxManagedBeanComponent=javax.annotation.ManagedBean +example.scannable.JavaxNamedComponent=javax.inject.Named example.indexed.IndexedJakartaManagedBeanComponent=jakarta.annotation.ManagedBean example.indexed.IndexedJakartaNamedComponent=jakarta.inject.Named +example.indexed.IndexedJavaxManagedBeanComponent=javax.annotation.ManagedBean +example.indexed.IndexedJavaxNamedComponent=javax.inject.Named diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml index df9bfadc8ebc..ae175f39a197 100644 --- a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml @@ -18,4 +18,6 @@ + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml index 175408a2cb39..d1a875359d3c 100644 --- a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml @@ -3,9 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> - @@ -27,39 +24,34 @@ - + - + + + + - + - + - + - + + String - - + Jenny 30 @@ -68,8 +60,7 @@ - + Simple bean, without any collections. diff --git a/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml index ea6901d7a269..eb9ffce842ae 100644 --- a/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml +++ b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml @@ -54,6 +54,7 @@ + @@ -93,6 +94,7 @@ + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties index a9fbccd9826a..8e65e9fc6d8a 100644 --- a/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties @@ -1 +1,2 @@ testbean.name=p4TestBean +from.p4=p4Value diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p5.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p5.properties new file mode 100644 index 000000000000..765adc0d34cc --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p5.properties @@ -0,0 +1,2 @@ +testbean.name=p5TestBean +from.p5=p5Value diff --git a/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-references.xml b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-references.xml new file mode 100644 index 000000000000..992ed41df19b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-references.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-expressions.xml b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-expressions.xml new file mode 100644 index 000000000000..8bdaddb2c02c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-expressions.xml @@ -0,0 +1,27 @@ + + + + + + + John Smith + 42 + Acme Widgets, Inc. + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-types.xml b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-types.xml new file mode 100644 index 000000000000..58422837ff52 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-types.xml @@ -0,0 +1,13 @@ + + + + + + + 42 + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values.xml b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values.xml new file mode 100644 index 000000000000..964f3c03e7f6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages.custom b/spring-context/src/test/resources/org/springframework/context/support/messages.custom new file mode 100644 index 000000000000..714793d2a366 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages.custom @@ -0,0 +1,2 @@ +code1=message1 +code2=message2 diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages_de.custom b/spring-context/src/test/resources/org/springframework/context/support/messages_de.custom new file mode 100644 index 000000000000..a9a00b17a0d7 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages_de.custom @@ -0,0 +1 @@ +code2=nachricht2 diff --git a/spring-context/src/test/resources/org/springframework/context/support/test/contextA.xml b/spring-context/src/test/resources/org/springframework/context/support/test/contextA.xml index 0ac88a5ffaa7..7a140cfdaad3 100644 --- a/spring-context/src/test/resources/org/springframework/context/support/test/contextA.xml +++ b/spring-context/src/test/resources/org/springframework/context/support/test/contextA.xml @@ -17,12 +17,18 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml b/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml index e8a47157a877..dd8f402278eb 100644 --- a/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml +++ b/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml @@ -40,14 +40,41 @@ - + + + + foo=bar + - + - + + foo=bar + + + - - + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyAssembling.xml b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyAssembling.xml index 7359d87ba4cf..7190ff538408 100644 --- a/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyAssembling.xml +++ b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyAssembling.xml @@ -20,7 +20,7 @@ - + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/autodetectLazyMBeans.xml b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectLazyMBeans.xml index e82e678dfcc9..a92ed936fca0 100644 --- a/spring-context/src/test/resources/org/springframework/jmx/export/autodetectLazyMBeans.xml +++ b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectLazyMBeans.xml @@ -13,7 +13,7 @@ - + diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java index 033c8e50d9d6..b119bc4556b0 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,9 +47,6 @@ */ public abstract class AbstractApplicationContextTests extends AbstractListableBeanFactoryTests { - /** Must be supplied as XML */ - public static final String TEST_NAMESPACE = "testNamespace"; - protected ConfigurableApplicationContext applicationContext; /** Subclass must register this */ @@ -59,17 +56,17 @@ public abstract class AbstractApplicationContextTests extends AbstractListableBe @BeforeEach - public void setup() throws Exception { + protected void setup() throws Exception { this.applicationContext = createContext(); } @Override protected BeanFactory getBeanFactory() { - return applicationContext; + return this.applicationContext; } protected ApplicationContext getApplicationContext() { - return applicationContext; + return this.applicationContext; } /** @@ -82,7 +79,7 @@ protected ApplicationContext getApplicationContext() { @Test - public void contextAwareSingletonWasCalledBack() throws Exception { + protected void contextAwareSingletonWasCalledBack() { ACATester aca = (ACATester) applicationContext.getBean("aca"); assertThat(aca.getApplicationContext()).as("has had context set").isSameAs(applicationContext); Object aca2 = applicationContext.getBean("aca"); @@ -91,7 +88,7 @@ public void contextAwareSingletonWasCalledBack() throws Exception { } @Test - public void contextAwarePrototypeWasCalledBack() throws Exception { + protected void contextAwarePrototypeWasCalledBack() { ACATester aca = (ACATester) applicationContext.getBean("aca-prototype"); assertThat(aca.getApplicationContext()).as("has had context set").isSameAs(applicationContext); Object aca2 = applicationContext.getBean("aca-prototype"); @@ -101,35 +98,35 @@ public void contextAwarePrototypeWasCalledBack() throws Exception { } @Test - public void parentNonNull() { + protected void parentNonNull() { assertThat(applicationContext.getParent()).as("parent isn't null").isNotNull(); } @Test - public void grandparentNull() { + protected void grandparentNull() { assertThat(applicationContext.getParent().getParent()).as("grandparent is null").isNull(); } @Test - public void overrideWorked() throws Exception { + protected void overrideWorked() { TestBean rod = (TestBean) applicationContext.getParent().getBean("rod"); assertThat(rod.getName().equals("Roderick")).as("Parent's name differs").isTrue(); } @Test - public void grandparentDefinitionFound() throws Exception { + protected void grandparentDefinitionFound() { TestBean dad = (TestBean) applicationContext.getBean("father"); assertThat(dad.getName().equals("Albert")).as("Dad has correct name").isTrue(); } @Test - public void grandparentTypedDefinitionFound() throws Exception { + protected void grandparentTypedDefinitionFound() { TestBean dad = applicationContext.getBean("father", TestBean.class); assertThat(dad.getName().equals("Albert")).as("Dad has correct name").isTrue(); } @Test - public void closeTriggersDestroy() { + protected void closeTriggersDestroy() { LifecycleBean lb = (LifecycleBean) applicationContext.getBean("lifecycle"); boolean condition = !lb.isDestroyed(); assertThat(condition).as("Not destroyed").isTrue(); @@ -146,7 +143,7 @@ public void closeTriggersDestroy() { } @Test - public void messageSource() throws NoSuchMessageException { + protected void messageSource() throws NoSuchMessageException { assertThat(applicationContext.getMessage("code1", null, Locale.getDefault())).isEqualTo("message1"); assertThat(applicationContext.getMessage("code2", null, Locale.getDefault())).isEqualTo("message2"); assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> @@ -154,12 +151,12 @@ public void messageSource() throws NoSuchMessageException { } @Test - public void events() throws Exception { + protected void events() throws Exception { doTestEvents(this.listener, this.parentListener, new MyEvent(this)); } @Test - public void eventsWithNoSource() throws Exception { + protected void eventsWithNoSource() throws Exception { // See SPR-10945 Serialized events result in a null source MyEvent event = new MyEvent(this); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -184,7 +181,7 @@ protected void doTestEvents(TestApplicationListener listener, TestApplicationLis } @Test - public void beanAutomaticallyHearsEvents() throws Exception { + protected void beanAutomaticallyHearsEvents() { //String[] listenerNames = ((ListableBeanFactory) applicationContext).getBeanDefinitionNames(ApplicationListener.class); //assertTrue("listeners include beanThatListens", Arrays.asList(listenerNames).contains("beanThatListens")); BeanThatListens b = (BeanThatListens) applicationContext.getBean("beanThatListens"); diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java index 66c3a66c7eaa..5a834ab04738 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -446,7 +446,7 @@ protected void testMultiCache(CacheableService service) { protected void testMultiEvict(CacheableService service) { Object o1 = new Object(); - Object o2 = o1.toString() + "A"; + Object o2 = o1 + "A"; Object r1 = service.multiCache(o1); @@ -504,6 +504,26 @@ protected void testPutRefersToResult(CacheableService service) { assertThat(primary.get(id).get()).isSameAs(entity); } + protected void testPutRefersToResultWithUnless(CacheableService service) { + Long id = 42L; + TestEntity entity = new TestEntity(); + entity.setId(id); + Cache primary = this.cm.getCache("primary"); + assertThat(primary.get(id)).isNull(); + assertThat(service.putEvaluatesUnlessBeforeKey(entity)).isNotNull(); + assertThat(primary.get(id).get()).isSameAs(entity); + } + + protected void testPutEvaluatesUnlessBeforeKey(CacheableService service) { + Long id = Long.MIN_VALUE; // return null + TestEntity entity = new TestEntity(); + entity.setId(id); + Cache primary = this.cm.getCache("primary"); + assertThat(primary.get(id)).isNull(); + assertThat(service.putEvaluatesUnlessBeforeKey(entity)).isNull(); + assertThat(primary.get(id)).isNull(); + } + protected void testMultiCacheAndEvict(CacheableService service) { String methodName = "multiCacheAndEvict"; @@ -538,7 +558,7 @@ protected void testMultiConditionalCacheAndEvict(CacheableService service) { Object r1 = service.multiConditionalCacheAndEvict(key); Object r3 = service.multiConditionalCacheAndEvict(key); - assertThat(!r1.equals(r3)).isTrue(); + assertThat(r1).isNotEqualTo(r3); assertThat(primary.get(key)).isNull(); Object key2 = 3; @@ -551,132 +571,132 @@ protected void testMultiConditionalCacheAndEvict(CacheableService service) { } @Test - public void testCacheable() { + protected void testCacheable() { testCacheable(this.cs); } @Test - public void testCacheableNull() { + protected void testCacheableNull() { testCacheableNull(this.cs); } @Test - public void testCacheableSync() { + protected void testCacheableSync() { testCacheableSync(this.cs); } @Test - public void testCacheableSyncNull() { + protected void testCacheableSyncNull() { testCacheableSyncNull(this.cs); } @Test - public void testEvict() { + protected void testEvict() { testEvict(this.cs, true); } @Test - public void testEvictEarly() { + protected void testEvictEarly() { testEvictEarly(this.cs); } @Test - public void testEvictWithException() { + protected void testEvictWithException() { testEvictException(this.cs); } @Test - public void testEvictAll() { + protected void testEvictAll() { testEvictAll(this.cs, true); } @Test - public void testEvictAllEarly() { + protected void testEvictAllEarly() { testEvictAllEarly(this.cs); } @Test - public void testEvictWithKey() { + protected void testEvictWithKey() { testEvictWithKey(this.cs); } @Test - public void testEvictWithKeyEarly() { + protected void testEvictWithKeyEarly() { testEvictWithKeyEarly(this.cs); } @Test - public void testConditionalExpression() { + protected void testConditionalExpression() { testConditionalExpression(this.cs); } @Test - public void testConditionalExpressionSync() { + protected void testConditionalExpressionSync() { testConditionalExpressionSync(this.cs); } @Test - public void testUnlessExpression() { + protected void testUnlessExpression() { testUnlessExpression(this.cs); } @Test - public void testClassCacheUnlessExpression() { + protected void testClassCacheUnlessExpression() { testUnlessExpression(this.cs); } @Test - public void testKeyExpression() { + protected void testKeyExpression() { testKeyExpression(this.cs); } @Test - public void testVarArgsKey() { + protected void testVarArgsKey() { testVarArgsKey(this.cs); } @Test - public void testClassCacheCacheable() { + protected void testClassCacheCacheable() { testCacheable(this.ccs); } @Test - public void testClassCacheEvict() { + protected void testClassCacheEvict() { testEvict(this.ccs, true); } @Test - public void testClassEvictEarly() { + protected void testClassEvictEarly() { testEvictEarly(this.ccs); } @Test - public void testClassEvictAll() { + protected void testClassEvictAll() { testEvictAll(this.ccs, true); } @Test - public void testClassEvictWithException() { + protected void testClassEvictWithException() { testEvictException(this.ccs); } @Test - public void testClassCacheEvictWithWKey() { + protected void testClassCacheEvictWithWKey() { testEvictWithKey(this.ccs); } @Test - public void testClassEvictWithKeyEarly() { + protected void testClassEvictWithKeyEarly() { testEvictWithKeyEarly(this.ccs); } @Test - public void testNullValue() { + protected void testNullValue() { testNullValue(this.cs); } @Test - public void testClassNullValue() { + protected void testClassNullValue() { Object key = new Object(); assertThat(this.ccs.nullValue(key)).isNull(); int nr = this.ccs.nullInvocations().intValue(); @@ -689,27 +709,27 @@ public void testClassNullValue() { } @Test - public void testMethodName() { + protected void testMethodName() { testMethodName(this.cs, "name"); } @Test - public void testClassMethodName() { + protected void testClassMethodName() { testMethodName(this.ccs, "nametestCache"); } @Test - public void testRootVars() { + protected void testRootVars() { testRootVars(this.cs); } @Test - public void testClassRootVars() { + protected void testClassRootVars() { testRootVars(this.ccs); } @Test - public void testCustomKeyGenerator() { + protected void testCustomKeyGenerator() { Object param = new Object(); Object r1 = this.cs.customKeyGenerator(param); assertThat(this.cs.customKeyGenerator(param)).isSameAs(r1); @@ -720,14 +740,14 @@ public void testCustomKeyGenerator() { } @Test - public void testUnknownCustomKeyGenerator() { + protected void testUnknownCustomKeyGenerator() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomKeyGenerator(param)); } @Test - public void testCustomCacheManager() { + protected void testCustomCacheManager() { CacheManager customCm = this.ctx.getBean("customCacheManager", CacheManager.class); Object key = new Object(); Object r1 = this.cs.customCacheManager(key); @@ -738,139 +758,159 @@ public void testCustomCacheManager() { } @Test - public void testUnknownCustomCacheManager() { + protected void testUnknownCustomCacheManager() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomCacheManager(param)); } @Test - public void testNullArg() { + protected void testNullArg() { testNullArg(this.cs); } @Test - public void testClassNullArg() { + protected void testClassNullArg() { testNullArg(this.ccs); } @Test - public void testCheckedException() { + protected void testCheckedException() { testCheckedThrowable(this.cs); } @Test - public void testClassCheckedException() { + protected void testClassCheckedException() { testCheckedThrowable(this.ccs); } @Test - public void testCheckedExceptionSync() { + protected void testCheckedExceptionSync() { testCheckedThrowableSync(this.cs); } @Test - public void testClassCheckedExceptionSync() { + protected void testClassCheckedExceptionSync() { testCheckedThrowableSync(this.ccs); } @Test - public void testUncheckedException() { + protected void testUncheckedException() { testUncheckedThrowable(this.cs); } @Test - public void testClassUncheckedException() { + protected void testClassUncheckedException() { testUncheckedThrowable(this.ccs); } @Test - public void testUncheckedExceptionSync() { + protected void testUncheckedExceptionSync() { testUncheckedThrowableSync(this.cs); } @Test - public void testClassUncheckedExceptionSync() { + protected void testClassUncheckedExceptionSync() { testUncheckedThrowableSync(this.ccs); } @Test - public void testUpdate() { + protected void testUpdate() { testCacheUpdate(this.cs); } @Test - public void testClassUpdate() { + protected void testClassUpdate() { testCacheUpdate(this.ccs); } @Test - public void testConditionalUpdate() { + protected void testConditionalUpdate() { testConditionalCacheUpdate(this.cs); } @Test - public void testClassConditionalUpdate() { + protected void testClassConditionalUpdate() { testConditionalCacheUpdate(this.ccs); } @Test - public void testMultiCache() { + protected void testMultiCache() { testMultiCache(this.cs); } @Test - public void testClassMultiCache() { + protected void testClassMultiCache() { testMultiCache(this.ccs); } @Test - public void testMultiEvict() { + protected void testMultiEvict() { testMultiEvict(this.cs); } @Test - public void testClassMultiEvict() { + protected void testClassMultiEvict() { testMultiEvict(this.ccs); } @Test - public void testMultiPut() { + protected void testMultiPut() { testMultiPut(this.cs); } @Test - public void testClassMultiPut() { + protected void testClassMultiPut() { testMultiPut(this.ccs); } @Test - public void testPutRefersToResult() { + protected void testPutRefersToResult() { testPutRefersToResult(this.cs); } @Test - public void testClassPutRefersToResult() { + protected void testPutRefersToResultWithUnless() { + testPutRefersToResultWithUnless(this.cs); + } + + @Test + protected void testPutEvaluatesUnlessBeforeKey() { + testPutEvaluatesUnlessBeforeKey(this.cs); + } + + @Test + protected void testClassPutRefersToResult() { testPutRefersToResult(this.ccs); } @Test - public void testMultiCacheAndEvict() { + protected void testClassPutRefersToResultWithUnless(){ + testPutRefersToResultWithUnless(this.ccs); + } + + @Test + protected void testClassPutEvaluatesUnlessBeforeKey(){ + testPutEvaluatesUnlessBeforeKey(this.ccs); + } + + @Test + protected void testMultiCacheAndEvict() { testMultiCacheAndEvict(this.cs); } @Test - public void testClassMultiCacheAndEvict() { + protected void testClassMultiCacheAndEvict() { testMultiCacheAndEvict(this.ccs); } @Test - public void testMultiConditionalCacheAndEvict() { + protected void testMultiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(this.cs); } @Test - public void testClassMultiConditionalCacheAndEvict() { + protected void testClassMultiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(this.ccs); } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheTests.java index c919e95e1108..bb3f43e5d196 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheTests.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ */ public abstract class AbstractCacheTests { - protected final static String CACHE_NAME = "testCache"; + protected static final String CACHE_NAME = "testCache"; protected abstract T getCache(); @@ -41,23 +41,23 @@ public abstract class AbstractCacheTests { @Test - public void testCacheName() throws Exception { + protected void testCacheName() { assertThat(getCache().getName()).isEqualTo(CACHE_NAME); } @Test - public void testNativeCache() throws Exception { + protected void testNativeCache() { assertThat(getCache().getNativeCache()).isSameAs(getNativeCache()); } @Test - public void testCachePut() throws Exception { + protected void testCachePut() { T cache = getCache(); String key = createRandomKey(); Object value = "george"; - assertThat((Object) cache.get(key)).isNull(); + assertThat(cache.get(key)).isNull(); assertThat(cache.get(key, String.class)).isNull(); assertThat(cache.get(key, Object.class)).isNull(); @@ -75,7 +75,7 @@ public void testCachePut() throws Exception { } @Test - public void testCachePutIfAbsent() throws Exception { + protected void testCachePutIfAbsent() { T cache = getCache(); String key = createRandomKey(); @@ -90,36 +90,36 @@ public void testCachePutIfAbsent() throws Exception { } @Test - public void testCacheRemove() throws Exception { + protected void testCacheRemove() { T cache = getCache(); String key = createRandomKey(); Object value = "george"; - assertThat((Object) cache.get(key)).isNull(); + assertThat(cache.get(key)).isNull(); cache.put(key, value); } @Test - public void testCacheClear() throws Exception { + protected void testCacheClear() { T cache = getCache(); - assertThat((Object) cache.get("enescu")).isNull(); + assertThat(cache.get("enescu")).isNull(); cache.put("enescu", "george"); - assertThat((Object) cache.get("vlaicu")).isNull(); + assertThat(cache.get("vlaicu")).isNull(); cache.put("vlaicu", "aurel"); cache.clear(); - assertThat((Object) cache.get("vlaicu")).isNull(); - assertThat((Object) cache.get("enescu")).isNull(); + assertThat(cache.get("vlaicu")).isNull(); + assertThat(cache.get("enescu")).isNull(); } @Test - public void testCacheGetCallable() { + protected void testCacheGetCallable() { doTestCacheGetCallable("test"); } @Test - public void testCacheGetCallableWithNull() { + protected void testCacheGetCallableWithNull() { doTestCacheGetCallable(null); } @@ -128,19 +128,19 @@ private void doTestCacheGetCallable(Object returnValue) { String key = createRandomKey(); - assertThat((Object) cache.get(key)).isNull(); + assertThat(cache.get(key)).isNull(); Object value = cache.get(key, () -> returnValue); assertThat(value).isEqualTo(returnValue); assertThat(cache.get(key).get()).isEqualTo(value); } @Test - public void testCacheGetCallableNotInvokedWithHit() { + protected void testCacheGetCallableNotInvokedWithHit() { doTestCacheGetCallableNotInvokedWithHit("existing"); } @Test - public void testCacheGetCallableNotInvokedWithHitNull() { + protected void testCacheGetCallableNotInvokedWithHitNull() { doTestCacheGetCallableNotInvokedWithHit(null); } @@ -157,11 +157,11 @@ private void doTestCacheGetCallableNotInvokedWithHit(Object initialValue) { } @Test - public void testCacheGetCallableFail() { + protected void testCacheGetCallableFail() { T cache = getCache(); String key = createRandomKey(); - assertThat((Object) cache.get(key)).isNull(); + assertThat(cache.get(key)).isNull(); try { cache.get(key, () -> { @@ -179,7 +179,7 @@ public void testCacheGetCallableFail() { * invocations. */ @Test - public void testCacheGetSynchronized() throws InterruptedException { + protected void testCacheGetSynchronized() throws InterruptedException { T cache = getCache(); final AtomicInteger counter = new AtomicInteger(); final List results = new CopyOnWriteArrayList<>(); diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractValueAdaptingCacheTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractValueAdaptingCacheTests.java index 5f0809deb187..087391417206 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractValueAdaptingCacheTests.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractValueAdaptingCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,14 +26,14 @@ * @author Stephane Nicoll */ public abstract class AbstractValueAdaptingCacheTests - extends AbstractCacheTests { + extends AbstractCacheTests { - protected final static String CACHE_NAME_NO_NULL = "testCacheNoNull"; + protected static final String CACHE_NAME_NO_NULL = "testCacheNoNull"; protected abstract T getCache(boolean allowNull); @Test - public void testCachePutNullValueAllowNullFalse() { + protected void testCachePutNullValueAllowNullFalse() { T cache = getCache(false); String key = createRandomKey(); assertThatIllegalArgumentException().isThrownBy(() -> diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java index aaf8fb68815d..f31441f192fd 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -205,7 +205,7 @@ public Object multiCache(Object arg1) { } @Override - @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#a0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) + @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#a0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) public Object multiEvict(Object arg1) { return this.counter.getAndIncrement(); } @@ -235,4 +235,10 @@ public TestEntity putRefersToResult(TestEntity arg1) { return arg1; } + @Override + @CachePut(cacheNames = "primary", key = "#result.id", unless = "#result == null") + public TestEntity putEvaluatesUnlessBeforeKey(TestEntity arg1) { + return (arg1.getId() != Long.MIN_VALUE ? arg1 : null); + } + } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java index 06afcea22f4d..0bcaed01b938 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -93,4 +93,6 @@ public interface CacheableService { TestEntity putRefersToResult(TestEntity arg1); + TestEntity putEvaluatesUnlessBeforeKey(TestEntity arg1); + } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java index 017a719173f8..87275a13d1b8 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -243,4 +243,10 @@ public TestEntity putRefersToResult(TestEntity arg1) { return arg1; } + @Override + @CachePut(cacheNames = "primary", key = "#result.id", unless = "#result == null") + public TestEntity putEvaluatesUnlessBeforeKey(TestEntity arg1) { + return (arg1.getId() != Long.MIN_VALUE ? arg1 : null); + } + } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java index 7a9d36fb4157..4fd7e9c0456d 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java @@ -16,6 +16,8 @@ package org.springframework.context.testfixture.cache.beans; +import java.util.Objects; + import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -38,7 +40,7 @@ public void setId(Long id) { @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.id); + return Objects.hashCode(this.id); } @Override diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceFieldComponent.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceFieldComponent.java new file mode 100644 index 000000000000..60a4dcedf771 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceFieldComponent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +public class LazyResourceFieldComponent { + + @Lazy + @Resource + private Environment environment; + + @Resource + private ResourceLoader resourceLoader; + + public Environment getEnvironment() { + return this.environment; + } + + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceMethodComponent.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceMethodComponent.java new file mode 100644 index 000000000000..e5bfb89551e8 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceMethodComponent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +public class LazyResourceMethodComponent { + + private Environment environment; + + private ResourceLoader resourceLoader; + + @Resource + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + public Environment getEnvironment() { + return this.environment; + } + + @Resource + @Lazy + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateFieldResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateFieldResourceSample.java new file mode 100644 index 000000000000..96658671e746 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateFieldResourceSample.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PackagePrivateFieldResourceSample { + + @Resource + String one; + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateMethodResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateMethodResourceSample.java new file mode 100644 index 000000000000..864fb5d6eaf7 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateMethodResourceSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PackagePrivateMethodResourceSample { + + private String one; + + @Resource + void setOne(String one) { + this.one = one; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateFieldResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateFieldResourceSample.java new file mode 100644 index 000000000000..bfeda526f271 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateFieldResourceSample.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PrivateFieldResourceSample { + + @Resource + private String one; + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceSample.java new file mode 100644 index 000000000000..7703e9773db9 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PrivateMethodResourceSample { + + private String one; + + @Resource + private void setOne(String one) { + this.one = one; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceWithCustomNameSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceWithCustomNameSample.java new file mode 100644 index 000000000000..f6350412d8c8 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceWithCustomNameSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PrivateMethodResourceWithCustomNameSample { + + private String text; + + @Resource(name = "one") + private void setText(String text) { + this.text = text; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PublicMethodResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PublicMethodResourceSample.java new file mode 100644 index 000000000000..87ba35f421d5 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PublicMethodResourceSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PublicMethodResourceSample { + + private String one; + + @Resource + public void setOne(String one) { + this.one = one; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/QualifierConfiguration.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/QualifierConfiguration.java index 19c0c37b7091..cf79c00bae9d 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/QualifierConfiguration.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/QualifierConfiguration.java @@ -49,4 +49,5 @@ public String two() { } } + } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ResourceComponent.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ResourceComponent.java new file mode 100644 index 000000000000..1cf1c7bd8ef6 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ResourceComponent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class ResourceComponent { + + private String text; + + private Integer counter; + + public String getText() { + return this.text; + } + + @Resource + public void setText(String text) { + this.text = text; + } + + public Integer getCounter() { + return this.counter; + } + + @Resource + public void setCounter(Integer counter) { + this.counter = counter; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateFieldResourceFromParentSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateFieldResourceFromParentSample.java new file mode 100644 index 000000000000..efa112e80bc4 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateFieldResourceFromParentSample.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation.subpkg; + +import org.springframework.context.testfixture.context.annotation.PackagePrivateFieldResourceSample; + +public class PackagePrivateFieldResourceFromParentSample extends PackagePrivateFieldResourceSample { + + // see one from parent +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateMethodResourceFromParentSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateMethodResourceFromParentSample.java new file mode 100644 index 000000000000..4cf5cad2487b --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateMethodResourceFromParentSample.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation.subpkg; + +import org.springframework.context.testfixture.context.annotation.PackagePrivateMethodResourceSample; + +public class PackagePrivateMethodResourceFromParentSample extends PackagePrivateMethodResourceSample { + + // see one from parent +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java index d578e407d9f3..d86dafb55329 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java @@ -22,7 +22,6 @@ import java.util.Enumeration; import java.util.stream.Stream; -import org.springframework.context.index.CandidateComponentsIndexLoader; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; @@ -40,7 +39,7 @@ public class CandidateComponentsTestClassLoader extends ClassLoader { * if resources are present at the standard location. * @param classLoader the classloader to use for all other operations * @return a test {@link ClassLoader} that has no index - * @see CandidateComponentsIndexLoader#COMPONENTS_RESOURCE_LOCATION + * @see org.springframework.context.index.CandidateComponentsIndexLoader#COMPONENTS_RESOURCE_LOCATION */ public static ClassLoader disableIndex(ClassLoader classLoader) { return new CandidateComponentsTestClassLoader(classLoader, Collections.emptyEnumeration()); @@ -86,8 +85,9 @@ public CandidateComponentsTestClassLoader(ClassLoader parent, IOException cause) } @Override + @SuppressWarnings({ "deprecation", "removal" }) public Enumeration getResources(String name) throws IOException { - if (CandidateComponentsIndexLoader.COMPONENTS_RESOURCE_LOCATION.equals(name)) { + if (org.springframework.context.index.CandidateComponentsIndexLoader.COMPONENTS_RESOURCE_LOCATION.equals(name)) { if (this.resourceUrls != null) { return this.resourceUrls; } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jmx/export/Person.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jmx/export/Person.java new file mode 100644 index 000000000000..391536b79dde --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jmx/export/Person.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.jmx.export; + +public class Person implements PersonMBean { + + private String name; + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jmx/export/PersonMBean.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jmx/export/PersonMBean.java new file mode 100644 index 000000000000..b0cba30aaf50 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jmx/export/PersonMBean.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.jmx.export; + +public interface PersonMBean { + + String getName(); + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java index bf3deedb368d..35610a92220f 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java @@ -479,7 +479,7 @@ public static ResourceBundle resourcebundlegetBundle(String baseName, Locale tar return result; } - public static ResourceBundle resourcebundlegetBundle( String baseName, Locale targetLocale, ResourceBundle.Control control) { + public static ResourceBundle resourcebundlegetBundle(String baseName, Locale targetLocale, ResourceBundle.Control control) { RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, targetLocale, control); ResourceBundle result = null; try { diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java b/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java index cc9fbecc41ef..375fdb2dbac5 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +64,7 @@ public static boolean isLoaded() { return loaded; } - private final static class ParsedArguments { + private static final class ParsedArguments { List instrumentedPackages; diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocations.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocations.java index 984d865a5098..6e21b07845f1 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocations.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,10 @@ public class RuntimeHintsInvocations implements AssertProvider T instantiateFactory(String implementationName, Class type, @Nullable ArgumentResolver argumentResolver, FailureHandler failureHandler) { diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoader.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoader.java index 35307a2716a8..768c18b9e211 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoader.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,8 +41,8 @@ * @author Sam Brannen * @since 6.0 */ +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @ExtendWith(CompileWithForkedClassLoaderExtension.class) public @interface CompileWithForkedClassLoader { diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java index 723a34bc0fb5..fe728b22acfa 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java @@ -70,7 +70,7 @@ public Class loadClass(String name) throws ClassNotFoundException { @Override protected Class findClass(String name) throws ClassNotFoundException { byte[] bytes = findClassBytes(name); - return (bytes != null) ? defineClass(name, bytes, 0, bytes.length, null) : super.findClass(name); + return (bytes != null ? defineClass(name, bytes, 0, bytes.length, null) : super.findClass(name)); } @Nullable diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.java index 8e63c3a372f5..73b88758a007 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.java @@ -85,15 +85,15 @@ public DynamicClassLoader(ClassLoader parent, ClassFiles classFiles, ResourceFil @Override protected Class findClass(String name) throws ClassNotFoundException { - byte[] bytes = findClassBytes(name); - if (bytes != null) { - return defineClass(name, bytes); - } - return super.findClass(name); + Class clazz = defineClass(name, findClassBytes(name)); + return (clazz != null ? clazz : super.findClass(name)); } - - private Class defineClass(String name, byte[] bytes) { + @Nullable + private Class defineClass(String name, @Nullable byte[] bytes) { + if (bytes == null) { + return null; + } if (this.defineClassMethod != null) { return (Class) ReflectionUtils.invokeMethod(this.defineClassMethod, getParent(), name, bytes, 0, bytes.length); @@ -139,9 +139,10 @@ private byte[] findClassBytes(String name) { return classFile.getContent(); } DynamicClassFileObject dynamicClassFile = this.dynamicClassFiles.get(name); - return (dynamicClassFile != null) ? dynamicClassFile.getBytes() : null; + return (dynamicClassFile != null ? dynamicClassFile.getBytes() : null); } + @SuppressWarnings("deprecation") // on JDK 20 private URL createResourceUrl(String name, Supplier bytesSupplier) { try { return new URL(null, "resource:///" + name, diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFile.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFile.java index 20592b20ec10..eb35e8a3609e 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFile.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,12 +86,10 @@ public static ResourceFile of(String path, WritableContent writableContent) { } /** - * AssertJ {@code assertThat} support. - * @deprecated use {@code assertThat(sourceFile)} rather than calling this - * method directly. + * Use {@code assertThat(sourceFile)} rather than calling this method + * directly. */ @Override - @Deprecated public ResourceFileAssert assertThat() { return new ResourceFileAssert(this); } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java index d4ddad6b9f2f..8db946050b88 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -218,12 +218,10 @@ else if (ch == ')') { } /** - * AssertJ {@code assertThat} support. - * @deprecated use {@code assertThat(sourceFile)} rather than calling this - * method directly. + * Use {@code assertThat(sourceFile)} rather than calling this method + * directly. */ @Override - @Deprecated public SourceFileAssert assertThat() { return new SourceFileAssert(this); } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java index 308118002d79..bb77051b9e7f 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.Locale; import java.util.function.Consumer; import java.util.function.UnaryOperator; +import java.util.stream.Stream; import javax.annotation.processing.Processor; import javax.tools.Diagnostic; @@ -41,6 +42,7 @@ * * @author Phillip Webb * @author Scott Frederick + * @author Stephane Nicoll * @since 6.0 * @see #forSystem() */ @@ -59,10 +61,12 @@ public final class TestCompiler { private final List processors; + private final List compilerOptions; + private TestCompiler(@Nullable ClassLoader classLoader, JavaCompiler compiler, SourceFiles sourceFiles, ResourceFiles resourceFiles, ClassFiles classFiles, - List processors) { + List processors, List compilerOptions) { this.classLoader = classLoader; this.compiler = compiler; @@ -70,6 +74,7 @@ private TestCompiler(@Nullable ClassLoader classLoader, JavaCompiler compiler, this.resourceFiles = resourceFiles; this.classFiles = classFiles; this.processors = processors; + this.compilerOptions = compilerOptions; } @@ -88,7 +93,7 @@ public static TestCompiler forSystem() { */ public static TestCompiler forCompiler(JavaCompiler javaCompiler) { return new TestCompiler(null, javaCompiler, SourceFiles.none(), - ResourceFiles.none(), ClassFiles.none(), Collections.emptyList()); + ResourceFiles.none(), ClassFiles.none(), Collections.emptyList(), Collections.emptyList()); } /** @@ -108,7 +113,7 @@ public TestCompiler with(UnaryOperator customizer) { public TestCompiler withSources(SourceFile... sourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles.and(sourceFiles), this.resourceFiles, - this.classFiles, this.processors); + this.classFiles, this.processors, this.compilerOptions); } /** @@ -119,7 +124,7 @@ public TestCompiler withSources(SourceFile... sourceFiles) { public TestCompiler withSources(Iterable sourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles.and(sourceFiles), this.resourceFiles, - this.classFiles, this.processors); + this.classFiles, this.processors, this.compilerOptions); } /** @@ -130,7 +135,7 @@ public TestCompiler withSources(Iterable sourceFiles) { public TestCompiler withSources(SourceFiles sourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles.and(sourceFiles), this.resourceFiles, - this.classFiles, this.processors); + this.classFiles, this.processors, this.compilerOptions); } /** @@ -140,7 +145,8 @@ public TestCompiler withSources(SourceFiles sourceFiles) { */ public TestCompiler withResources(ResourceFile... resourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles.and(resourceFiles), this.classFiles, this.processors); + this.resourceFiles.and(resourceFiles), this.classFiles, this.processors, + this.compilerOptions); } /** @@ -150,7 +156,8 @@ public TestCompiler withResources(ResourceFile... resourceFiles) { */ public TestCompiler withResources(Iterable resourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles.and(resourceFiles), this.classFiles, this.processors); + this.resourceFiles.and(resourceFiles), this.classFiles, this.processors, + this.compilerOptions); } /** @@ -160,7 +167,8 @@ public TestCompiler withResources(Iterable resourceFiles) { */ public TestCompiler withResources(ResourceFiles resourceFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles.and(resourceFiles), this.classFiles, this.processors); + this.resourceFiles.and(resourceFiles), this.classFiles, this.processors, + this.compilerOptions); } /** @@ -170,7 +178,8 @@ public TestCompiler withResources(ResourceFiles resourceFiles) { */ public TestCompiler withClasses(Iterable classFiles) { return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles, this.classFiles.and(classFiles), this.processors); + this.resourceFiles, this.classFiles.and(classFiles), this.processors, + this.compilerOptions); } /** @@ -182,7 +191,7 @@ public TestCompiler withProcessors(Processor... processors) { List mergedProcessors = new ArrayList<>(this.processors); mergedProcessors.addAll(Arrays.asList(processors)); return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles, this.classFiles, mergedProcessors); + this.resourceFiles, this.classFiles, mergedProcessors, this.compilerOptions); } /** @@ -194,7 +203,32 @@ public TestCompiler withProcessors(Iterable processors) { List mergedProcessors = new ArrayList<>(this.processors); processors.forEach(mergedProcessors::add); return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, - this.resourceFiles, this.classFiles, mergedProcessors); + this.resourceFiles, this.classFiles, mergedProcessors, this.compilerOptions); + } + + /** + * Create a new {@link TestCompiler} instance with the additional compiler options. + * @param options the additional compiler options + * @return a new {@code TestCompiler} instance + * @since 6.1 + */ + public TestCompiler withCompilerOptions(String... options) { + List mergedCompilerOptions = Stream.concat(this.compilerOptions.stream(), + Arrays.stream(options)).distinct().toList(); + return new TestCompiler(this.classLoader, this.compiler, this.sourceFiles, + this.resourceFiles, this.classFiles, this.processors, mergedCompilerOptions); + } + + /** + * Create a new {@link TestCompiler} instance that fails if any warning is + * encountered. This sets the {@code -Xlint:all} and {@code -Werror} compiler + * options. + * @return a new {@code TestCompiler} instance + * @since 6.1 + * @see #withCompilerOptions(String...) + */ + public TestCompiler failOnWarning() { + return withCompilerOptions("-Xlint:all", "-Werror"); } /** @@ -265,8 +299,8 @@ public void compile(Consumer compiled) throws CompilationException { } private DynamicClassLoader compile() { - ClassLoader classLoaderToUse = (this.classLoader != null) ? this.classLoader - : Thread.currentThread().getContextClassLoader(); + ClassLoader classLoaderToUse = (this.classLoader != null ? this.classLoader + : Thread.currentThread().getContextClassLoader()); List compilationUnits = this.sourceFiles.stream().map( DynamicJavaFileObject::new).toList(); StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager( @@ -275,11 +309,9 @@ private DynamicClassLoader compile() { standardFileManager, classLoaderToUse, this.classFiles, this.resourceFiles); if (!this.sourceFiles.isEmpty()) { Errors errors = new Errors(); - CompilationTask task = this.compiler.getTask(null, fileManager, errors, null, - null, compilationUnits); - if (!this.processors.isEmpty()) { - task.setProcessors(this.processors); - } + CompilationTask task = this.compiler.getTask(null, fileManager, errors, + this.compilerOptions, null, compilationUnits); + task.setProcessors(this.processors); boolean result = task.call(); if (!result || errors.hasReportedErrors()) { throw new CompilationException(errors.toString(), this.sourceFiles, this.resourceFiles); @@ -333,7 +365,7 @@ public void report(Diagnostic diagnostic) { } boolean hasReportedErrors() { - return this.message.length() > 0; + return !this.message.isEmpty(); } @Override diff --git a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java index 9a2edab4a463..48a9b68652e6 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,16 +40,16 @@ */ class InstrumentedMethodTests { - private RuntimeHints hints = new RuntimeHints(); + private final RuntimeHints hints = new RuntimeHints(); @Nested class ClassReflectionInstrumentationTests { - RecordedInvocation stringGetClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCLASSES) + final RecordedInvocation stringGetClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCLASSES) .onInstance(String.class).returnValue(String.class.getClasses()).build(); - RecordedInvocation stringGetDeclaredClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCLASSES) + final RecordedInvocation stringGetDeclaredClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCLASSES) .onInstance(String.class).returnValue(String.class.getDeclaredClasses()).build(); @Test @@ -106,12 +106,12 @@ class ConstructorReflectionInstrumentationTests { RecordedInvocation stringGetConstructor; - RecordedInvocation stringGetConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTORS) + final RecordedInvocation stringGetConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTORS) .onInstance(String.class).returnValue(String.class.getConstructors()).build(); RecordedInvocation stringGetDeclaredConstructor; - RecordedInvocation stringGetDeclaredConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS) + final RecordedInvocation stringGetDeclaredConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS) .onInstance(String.class).returnValue(String.class.getDeclaredConstructors()).build(); @BeforeEach @@ -124,7 +124,7 @@ public void setup() throws Exception { } @Test - void classGetConstructorShouldMatchInstrospectPublicConstructorsHint() { + void classGetConstructorShouldMatchIntrospectPublicConstructorsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); } @@ -148,7 +148,7 @@ void classGetConstructorShouldMatchInvokeDeclaredConstructorsHint() { } @Test - void classGetConstructorShouldMatchInstrospectConstructorHint() { + void classGetConstructorShouldMatchIntrospectConstructorHint() { hints.reflection().registerType(String.class,typeHint -> typeHint.withConstructor(Collections.emptyList(), ExecutableMode.INTROSPECT)); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); @@ -210,7 +210,7 @@ void classGetDeclaredConstructorShouldNotMatchIntrospectPublicConstructorsHint() } @Test - void classGetDeclaredConstructorShouldMatchInstrospectConstructorHint() { + void classGetDeclaredConstructorShouldMatchIntrospectConstructorHint() { hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(TypeReference.listOf(byte[].class, byte.class), ExecutableMode.INTROSPECT)); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); @@ -354,13 +354,13 @@ void classGetDeclaredMethodsShouldNotMatchMethodReflectionHint() throws Exceptio } @Test - void classGetMethodsShouldMatchInstrospectDeclaredMethodsHint() { + void classGetMethodsShouldMatchIntrospectDeclaredMethodsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @Test - void classGetMethodsShouldMatchInstrospectPublicMethodsHint() { + void classGetMethodsShouldMatchIntrospectPublicMethodsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @@ -396,7 +396,7 @@ void classGetMethodsShouldNotMatchMethodReflectionHint() throws Exception { } @Test - void classGetMethodShouldMatchInstrospectPublicMethodsHint() { + void classGetMethodShouldMatchIntrospectPublicMethodsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } @@ -434,13 +434,13 @@ void classGetMethodShouldMatchInvokeMethodHint() { } @Test - void classGetMethodShouldNotMatchInstrospectPublicMethodsHintWhenPrivate() throws Exception { + void classGetMethodShouldNotMatchIntrospectPublicMethodsHintWhenPrivate() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod); } @Test - void classGetMethodShouldMatchInstrospectDeclaredMethodsHintWhenPrivate() { + void classGetMethodShouldMatchIntrospectDeclaredMethodsHintWhenPrivate() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod); } @@ -556,7 +556,7 @@ void classGetFieldShouldMatchFieldHint() { } @Test - void classGetFieldShouldNotMatchPublicFieldsHintWhenPrivate() throws NoSuchFieldException { + void classGetFieldShouldNotMatchPublicFieldsHintWhenPrivate() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) .onInstance(String.class).withArgument("value").returnValue(null).build(); hints.reflection().registerType(String.class, MemberCategory.PUBLIC_FIELDS); @@ -572,7 +572,7 @@ void classGetFieldShouldMatchDeclaredFieldsHintWhenPrivate() throws NoSuchFieldE } @Test - void classGetFieldShouldNotMatchForWrongType() throws Exception { + void classGetFieldShouldNotMatchForWrongType() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) .onInstance(String.class).withArgument("value").returnValue(null).build(); hints.reflection().registerType(Integer.class, MemberCategory.DECLARED_FIELDS); diff --git a/spring-core-test/src/test/java/org/springframework/aot/agent/RecordedInvocationTests.java b/spring-core-test/src/test/java/org/springframework/aot/agent/RecordedInvocationTests.java index 35b633dd810f..0a2410a4d7b9 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/agent/RecordedInvocationTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/agent/RecordedInvocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-core-test/src/test/java/org/springframework/core/test/io/support/MockSpringFactoriesLoaderTests.java b/spring-core-test/src/test/java/org/springframework/core/test/io/support/MockSpringFactoriesLoaderTests.java index c8a469fbb720..a4349c084086 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/io/support/MockSpringFactoriesLoaderTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/io/support/MockSpringFactoriesLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,9 +61,9 @@ void addWithClassNameAndInstancesAddsFactories() { private void assertThatLoaderHasTestFactories(MockSpringFactoriesLoader loader) { List factories = loader.load(TestFactoryType.class); - assertThat(factories).hasSize(2); - assertThat(factories.get(0)).isInstanceOf(TestFactoryOne.class); - assertThat(factories.get(1)).isInstanceOf(TestFactoryTwo.class); + assertThat(factories).satisfiesExactly( + zero -> assertThat(zero).isInstanceOf(TestFactoryOne.class), + one -> assertThat(one).isInstanceOf(TestFactoryTwo.class)); } interface TestFactoryType { diff --git a/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFileTests.java b/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFileTests.java index 353c9dbdae5e..79c43b6b0939 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFileTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ */ class ClassFileTests { - private final static byte[] TEST_CONTENT = new byte[]{'a'}; + private static final byte[] TEST_CONTENT = new byte[]{'a'}; @Test void ofNameAndByteArrayCreatesClass() { diff --git a/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFilesTests.java b/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFilesTests.java index c43522bf0661..d28a1a335ee8 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFilesTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/tools/ClassFilesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ class ClassFilesTests { void noneReturnsNone() { ClassFiles none = ClassFiles.none(); assertThat(none).isNotNull(); - assertThat(none.isEmpty()).isTrue(); + assertThat(none).isEmpty(); } @Test @@ -83,13 +83,13 @@ void streamStreamsClassFiles() { @Test void isEmptyWhenEmptyReturnsTrue() { ClassFiles classFiles = ClassFiles.of(); - assertThat(classFiles.isEmpty()).isTrue(); + assertThat(classFiles).isEmpty(); } @Test void isEmptyWhenNotEmptyReturnsFalse() { ClassFiles classFiles = ClassFiles.of(CLASS_FILE_1); - assertThat(classFiles.isEmpty()).isFalse(); + assertThat(classFiles).isNotEmpty(); } @Test diff --git a/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFileTests.java b/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFileTests.java index 6c96a6201d7c..844262c0c210 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFileTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,10 +42,9 @@ void ofPathAndWritableContentCreatesResource() { } @Test - @SuppressWarnings("deprecation") - void assertThatReturnsResourceFileAssert() { - ResourceFile file = ResourceFile.of("path", "test"); - assertThat(file.assertThat()).isInstanceOf(ResourceFileAssert.class); + void assertThatUsesResourceFileAssert() { + ResourceFile file = ResourceFile.of("path", appendable -> appendable.append("test")); + assertThat(file).hasContent("test"); } } diff --git a/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFilesTests.java b/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFilesTests.java index 7d98747b6025..0b599a6f2e36 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFilesTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/tools/ResourceFilesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class ResourceFilesTests { void noneReturnsNone() { ResourceFiles none = ResourceFiles.none(); assertThat(none).isNotNull(); - assertThat(none.isEmpty()).isTrue(); + assertThat(none).isEmpty(); } @Test @@ -85,13 +85,13 @@ void streamStreamsResourceFiles() { @Test void isEmptyWhenEmptyReturnsTrue() { ResourceFiles resourceFiles = ResourceFiles.of(); - assertThat(resourceFiles.isEmpty()).isTrue(); + assertThat(resourceFiles).isEmpty(); } @Test void isEmptyWhenNotEmptyReturnsFalse() { ResourceFiles resourceFiles = ResourceFiles.of(RESOURCE_FILE_1); - assertThat(resourceFiles.isEmpty()).isFalse(); + assertThat(resourceFiles).isNotEmpty(); } @Test diff --git a/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFileTests.java b/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFileTests.java index 5280c9f79528..488806af31ae 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFileTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -106,10 +106,9 @@ void getContentReturnsContent() { } @Test - @SuppressWarnings("deprecation") - void assertThatReturnsAssert() { + void assertThatUseSourceFileAssert() { SourceFile sourceFile = SourceFile.of(HELLO_WORLD); - assertThat(sourceFile.assertThat()).isInstanceOf(SourceFileAssert.class); + assertThat(sourceFile).hasContent(HELLO_WORLD); } @Test diff --git a/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFilesTests.java b/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFilesTests.java index dce77f845c26..7e6460986309 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFilesTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/tools/SourceFilesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class SourceFilesTests { void noneReturnsNone() { SourceFiles none = SourceFiles.none(); assertThat(none).isNotNull(); - assertThat(none.isEmpty()).isTrue(); + assertThat(none).isEmpty(); } @Test @@ -84,13 +84,13 @@ void streamStreamsSourceFiles() { @Test void isEmptyWhenEmptyReturnsTrue() { SourceFiles sourceFiles = SourceFiles.of(); - assertThat(sourceFiles.isEmpty()).isTrue(); + assertThat(sourceFiles).isEmpty(); } @Test void isEmptyWhenNotEmptyReturnsFalse() { SourceFiles sourceFiles = SourceFiles.of(SOURCE_FILE_1); - assertThat(sourceFiles.isEmpty()).isFalse(); + assertThat(sourceFiles).isNotEmpty(); } @Test diff --git a/spring-core-test/src/test/java/org/springframework/core/test/tools/TestCompilerTests.java b/spring-core-test/src/test/java/org/springframework/core/test/tools/TestCompilerTests.java index 63a3055c5ee5..48bd7e8df73e 100644 --- a/spring-core-test/src/test/java/org/springframework/core/test/tools/TestCompilerTests.java +++ b/spring-core-test/src/test/java/org/springframework/core/test/tools/TestCompilerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Scott Frederick + * @author Stephane Nicoll */ class TestCompilerTests { @@ -87,6 +88,20 @@ public String get() { } """; + private static final String HELLO_DEPRECATED = """ + package com.example; + + import java.util.function.Supplier; + + public class Hello implements Supplier { + + @Deprecated + public String get() { + return "Hello Deprecated"; + } + + } + """; @Test @SuppressWarnings("unchecked") @@ -119,6 +134,70 @@ void compileWhenSourceHasCompileErrors() { })); } + @Test + @SuppressWarnings("unchecked") + void compileWhenSourceUseDeprecateCodeAndNoOptionSet() { + SourceFile main = SourceFile.of(""" + package com.example; + + public class Main { + + public static void main(String[] args) { + new Hello().get(); + } + + } + """); + TestCompiler.forSystem().withSources( + SourceFile.of(HELLO_DEPRECATED), main).compile(compiled -> { + Supplier supplier = compiled.getInstance(Supplier.class, + "com.example.Hello"); + assertThat(supplier.get()).isEqualTo("Hello Deprecated"); + }); + } + + @Test + void compileWhenSourceUseDeprecateCodeAndFailOnWarningIsSet() { + SourceFile main = SourceFile.of(""" + package com.example; + + public class Main { + + public static void main(String[] args) { + new Hello().get(); + } + + } + """); + assertThatExceptionOfType(CompilationException.class).isThrownBy( + () -> TestCompiler.forSystem().failOnWarning().withSources( + SourceFile.of(HELLO_DEPRECATED), main).compile(compiled -> { + })); + } + + @Test + @SuppressWarnings("unchecked") + void compileWhenSourceUseDeprecateCodeAndFailOnWarningWithSuppressWarnings() { + SourceFile main = SourceFile.of(""" + package com.example; + + public class Main { + + @SuppressWarnings("deprecation") + public static void main(String[] args) { + new Hello().get(); + } + + } + """); + TestCompiler.forSystem().failOnWarning().withSources( + SourceFile.of(HELLO_DEPRECATED), main).compile(compiled -> { + Supplier supplier = compiled.getInstance(Supplier.class, + "com.example.Hello"); + assertThat(supplier.get()).isEqualTo("Hello Deprecated"); + }); + } + @Test void withSourcesArrayAddsSource() { SourceFile sourceFile = SourceFile.of(HELLO_WORLD); diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index f2e109e49a58..ba7057d50e50 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -1,15 +1,25 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.springframework.build.shadow.ShadowSource +plugins { + id 'me.champeau.mrjar' +} + description = "Spring Core" apply plugin: "kotlin" apply plugin: "kotlinx-serialization" +multiRelease { + targetVersions 17, 21 +} + def javapoetVersion = "1.13.0" -def objenesisVersion = "3.3" +def objenesisVersion = "3.4" configurations { + java21Api.extendsFrom(api) + java21Implementation.extendsFrom(implementation) javapoet objenesis graalvm @@ -63,6 +73,7 @@ dependencies { api(project(":spring-jcl")) compileOnly("io.projectreactor.tools:blockhound") compileOnly("org.graalvm.sdk:graal-sdk") + optional("io.micrometer:context-propagation") optional("io.netty:netty-buffer") optional("io.netty:netty5-buffer") optional("io.projectreactor:reactor-core") diff --git a/spring-core/src/jmh/java/org/springframework/core/env/CompositePropertySourceBenchmark.java b/spring-core/src/jmh/java/org/springframework/core/env/CompositePropertySourceBenchmark.java new file mode 100644 index 000000000000..593403516dd2 --- /dev/null +++ b/spring-core/src/jmh/java/org/springframework/core/env/CompositePropertySourceBenchmark.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.env; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import org.springframework.util.AlternativeJdkIdGenerator; +import org.springframework.util.IdGenerator; + +/** + * Benchmarks for {@link CompositePropertySource}. + * + * @author Yike Xiao + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 2) +public class CompositePropertySourceBenchmark { + + @Benchmark + public void getPropertyNames(BenchmarkState state, Blackhole blackhole) { + blackhole.consume(state.composite.getPropertyNames()); + } + + @State(Scope.Benchmark) + public static class BenchmarkState { + + private static final IdGenerator ID_GENERATOR = new AlternativeJdkIdGenerator(); + + private static final Object VALUE = new Object(); + + CompositePropertySource composite; + + @Param({ "2", "5", "10" }) + int numberOfPropertySources; + + @Param({ "10", "100", "1000" }) + int numberOfPropertyNamesPerSource; + + @Setup(Level.Trial) + public void setUp() { + this.composite = new CompositePropertySource("benchmark"); + for (int i = 0; i < this.numberOfPropertySources; i++) { + Map map = new HashMap<>(this.numberOfPropertyNamesPerSource); + for (int j = 0; j < this.numberOfPropertyNamesPerSource; j++) { + map.put(ID_GENERATOR.generateId().toString(), VALUE); + } + PropertySource propertySource = new MapPropertySource("propertySource" + i, map); + this.composite.addPropertySource(propertySource); + } + } + + } + +} diff --git a/spring-core/src/jmh/java/org/springframework/util/ConcurrentReferenceHashMapBenchmark.java b/spring-core/src/jmh/java/org/springframework/util/ConcurrentReferenceHashMapBenchmark.java new file mode 100644 index 000000000000..31302a2b1b88 --- /dev/null +++ b/spring-core/src/jmh/java/org/springframework/util/ConcurrentReferenceHashMapBenchmark.java @@ -0,0 +1,118 @@ +/* + * 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.util; + + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.WeakHashMap; +import java.util.function.Function; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmarks for {@link ConcurrentReferenceHashMap}. + *

This benchmark ensures that {@link ConcurrentReferenceHashMap} performs + * better than {@link java.util.Collections#synchronizedMap(Map)} with + * concurrent read operations. + *

Typically this can be run with {@code "java -jar spring-core-jmh.jar -t 30 -f 2 ConcurrentReferenceHashMapBenchmark"}. + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class ConcurrentReferenceHashMapBenchmark { + + @Benchmark + public void concurrentMap(ConcurrentMapBenchmarkData data, Blackhole bh) { + for (String element : data.elements) { + WeakReference value = data.map.get(element); + bh.consume(value); + } + } + + @State(Scope.Benchmark) + public static class ConcurrentMapBenchmarkData { + + @Param({"500"}) + public int capacity; + private final Function generator = key -> key + "value"; + + public List elements; + + public Map> map; + + @Setup(Level.Iteration) + public void setup() { + this.elements = new ArrayList<>(this.capacity); + this.map = new ConcurrentReferenceHashMap<>(); + Random random = new Random(); + random.ints(this.capacity).forEach(value -> { + String element = String.valueOf(value); + this.elements.add(element); + this.map.put(element, new WeakReference<>(this.generator.apply(element))); + }); + this.elements.sort(String::compareTo); + } + } + + @Benchmark + public void synchronizedMap(SynchronizedMapBenchmarkData data, Blackhole bh) { + for (String element : data.elements) { + WeakReference value = data.map.get(element); + bh.consume(value); + } + } + + @State(Scope.Benchmark) + public static class SynchronizedMapBenchmarkData { + + @Param({"500"}) + public int capacity; + + private Function generator = key -> key + "value"; + + public List elements; + + public Map> map; + + + @Setup(Level.Iteration) + public void setup() { + this.elements = new ArrayList<>(this.capacity); + this.map = Collections.synchronizedMap(new WeakHashMap<>()); + Random random = new Random(); + random.ints(this.capacity).forEach(value -> { + String element = String.valueOf(value); + this.elements.add(element); + this.map.put(element, new WeakReference<>(this.generator.apply(element))); + }); + this.elements.sort(String::compareTo); + } + } + +} diff --git a/spring-core/src/jmh/java/org/springframework/util/StringUtilsBenchmark.java b/spring-core/src/jmh/java/org/springframework/util/StringUtilsBenchmark.java index 1fc7753b2218..6fa0a2a4fa51 100644 --- a/spring-core/src/jmh/java/org/springframework/util/StringUtilsBenchmark.java +++ b/spring-core/src/jmh/java/org/springframework/util/StringUtilsBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,7 +100,7 @@ public void setup() { } private String createSamplePath(Random random) { - String separator = random.nextBoolean() ? "/" : "\\"; + String separator = (random.nextBoolean() ? "/" : "\\"); StringBuilder sb = new StringBuilder(); sb.append("jar:file:///c:"); for (int i = 0; i < this.segmentCount; i++) { diff --git a/spring-core/src/main/java/org/springframework/aot/generate/AccessControl.java b/spring-core/src/main/java/org/springframework/aot/generate/AccessControl.java index 3f4cd4018511..cb9cbd4ec195 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/AccessControl.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/AccessControl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -215,7 +215,7 @@ private static Visibility forClass(Class clazz) { clazz = ClassUtils.getUserClass(clazz); Visibility visibility = forModifiers(clazz.getModifiers()); if (clazz.isArray()) { - visibility = lowest(visibility, forClass(clazz.getComponentType())); + visibility = lowest(visibility, forClass(clazz.componentType())); } Class enclosingClass = clazz.getEnclosingClass(); if (enclosingClass != null) { diff --git a/spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java b/spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java index faf34eb53d47..fab5a7460c12 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.aot.generate; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -42,7 +41,7 @@ class AppendableConsumerInputStreamSource implements InputStreamSource { @Override - public InputStream getInputStream() throws IOException { + public InputStream getInputStream() { return new ByteArrayInputStream(toString().getBytes(StandardCharsets.UTF_8)); } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java b/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java index ca512fb27d0b..3018c6407cfc 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ public DefaultMethodReference(MethodSpec method, @Nullable ClassName declaringCl public CodeBlock toCodeBlock() { String methodName = this.method.name; if (isStatic()) { - Assert.state(this.declaringClass != null, "static method reference must define a declaring class"); + Assert.state(this.declaringClass != null, "Static method reference must define a declaring class"); return CodeBlock.of("$T::$L", this.declaringClass, methodName); } else { @@ -64,11 +64,12 @@ public CodeBlock toCodeBlock() { @Override public CodeBlock toInvokeCodeBlock(ArgumentCodeGenerator argumentCodeGenerator, @Nullable ClassName targetClassName) { + String methodName = this.method.name; CodeBlock.Builder code = CodeBlock.builder(); if (isStatic()) { - Assert.state(this.declaringClass != null, "static method reference must define a declaring class"); - if (isSameDeclaringClass(targetClassName)) { + Assert.state(this.declaringClass != null, "Static method reference must define a declaring class"); + if (this.declaringClass.equals(targetClassName)) { code.add("$L", methodName); } else { @@ -76,7 +77,7 @@ public CodeBlock toInvokeCodeBlock(ArgumentCodeGenerator argumentCodeGenerator, } } else { - if (!isSameDeclaringClass(targetClassName)) { + if (this.declaringClass != null && !this.declaringClass.equals(targetClassName)) { code.add(instantiateDeclaringClass(this.declaringClass)); } code.add("$L", methodName); @@ -117,10 +118,6 @@ private boolean isStatic() { return this.method.modifiers.contains(Modifier.STATIC); } - private boolean isSameDeclaringClass(ClassName declaringClass) { - return this.declaringClass == null || this.declaringClass.equals(declaringClass); - } - @Override public String toString() { String methodName = this.method.name; @@ -128,7 +125,7 @@ public String toString() { return this.declaringClass + "::" + methodName; } else { - return ((this.declaringClass != null) ? + return (this.declaringClass != null ? "<" + this.declaringClass + ">" : "") + "::" + methodName; } } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/Generated.java b/spring-core/src/main/java/org/springframework/aot/generate/Generated.java new file mode 100644 index 000000000000..95574e248085 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/Generated.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicate that the type has been generated ahead of time. + * + * @author Stephane Nicoll + * @since 6.1.2 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Generated { +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java index 6917e5051e48..2b1caa6d1365 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ public void reserveMethodNames(String... reservedMethodNames) { private String generateSequencedMethodName(MethodName name) { int sequence = this.methodNameSequenceGenerator .computeIfAbsent(name, key -> new AtomicInteger()).getAndIncrement(); - return (sequence > 0) ? name.toString() + sequence : name.toString(); + return (sequence > 0 ? name.toString() + sequence : name.toString()); } /** @@ -142,6 +142,7 @@ JavaFile generateJavaFile() { private TypeSpec.Builder apply() { TypeSpec.Builder type = getBuilder(this.type); + type.addAnnotation(Generated.class); this.methods.doWithMethodSpecs(type::addMethod); this.declaredClasses.values().forEach(declaredClass -> type.addType(declaredClass.apply().build())); diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java index bd6ec60ae27c..1c326e4fd96d 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java @@ -20,6 +20,7 @@ import org.springframework.javapoet.JavaFile; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.util.function.ThrowingConsumer; /** @@ -43,6 +44,7 @@ public interface GeneratedFiles { * @param javaFile the java file to add */ default void addSourceFile(JavaFile javaFile) { + validatePackage(javaFile.packageName, javaFile.typeSpec.name); String className = javaFile.packageName + "." + javaFile.typeSpec.name; addSourceFile(className, javaFile::writeTo); } @@ -161,11 +163,20 @@ default void addFile(Kind kind, String path, ThrowingConsumer conten private static String getClassNamePath(String className) { Assert.hasLength(className, "'className' must not be empty"); + validatePackage(ClassUtils.getPackageName(className), className); Assert.isTrue(isJavaIdentifier(className), "'className' must be a valid identifier, got '" + className + "'"); return ClassUtils.convertClassNameToResourcePath(className) + ".java"; } + private static void validatePackage(String packageName, String className) { + if (!StringUtils.hasLength(packageName)) { + throw new IllegalArgumentException("Could not add '" + className + "', " + + "processing classes in the default package is not supported. " + + "Did you forget to add a package statement?"); + } + } + private static boolean isJavaIdentifier(String className) { char[] chars = className.toCharArray(); for (int i = 0; i < chars.length; i++) { diff --git a/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java index 00f1fe2d7b54..f3936c21be7f 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,7 @@ public InputStreamSource getGeneratedFile(Kind kind, String path) { Assert.notNull(kind, "'kind' must not be null"); Assert.hasLength(path, "'path' must not be empty"); Map paths = this.files.get(kind); - return (paths != null) ? paths.get(path) : null; + return (paths != null ? paths.get(path) : null); } } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java b/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java index f9ea24c9fe87..6a4ff72f6f97 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java @@ -116,7 +116,7 @@ private static String clean(@Nullable String part) { StringBuilder name = new StringBuilder(chars.length); boolean uppercase = false; for (char ch : chars) { - char outputChar = (!uppercase) ? ch : Character.toUpperCase(ch); + char outputChar = (!uppercase ? ch : Character.toUpperCase(ch)); name.append((!Character.isLetter(ch)) ? "" : outputChar); uppercase = (ch == '.'); } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/UnsupportedTypeValueCodeGenerationException.java b/spring-core/src/main/java/org/springframework/aot/generate/UnsupportedTypeValueCodeGenerationException.java new file mode 100644 index 000000000000..e2fe95887feb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/UnsupportedTypeValueCodeGenerationException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +/** + * Thrown when a {@link ValueCodeGenerator} could not generate the code for a + * given value. + * + * @author Stephane Nicoll + * @since 6.1.2 + */ +@SuppressWarnings("serial") +public class UnsupportedTypeValueCodeGenerationException extends ValueCodeGenerationException { + + public UnsupportedTypeValueCodeGenerationException(Object value) { + super("Code generation does not support " + value.getClass().getName(), value, null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java new file mode 100644 index 000000000000..5aaa9ba513a9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import org.springframework.lang.Nullable; + +/** + * Thrown when value code generation fails. + * + * @author Stephane Nicoll + * @since 6.1.2 + */ +@SuppressWarnings("serial") +public class ValueCodeGenerationException extends RuntimeException { + + @Nullable + private final Object value; + + protected ValueCodeGenerationException(String message, @Nullable Object value, @Nullable Throwable cause) { + super(message, cause); + this.value = value; + } + + public ValueCodeGenerationException(@Nullable Object value, Throwable cause) { + super(buildErrorMessage(value), cause); + this.value = value; + } + + private static String buildErrorMessage(@Nullable Object value) { + StringBuilder message = new StringBuilder("Failed to generate code for '"); + message.append(value).append("'"); + if (value != null) { + message.append(" with type ").append(value.getClass()); + } + return message.toString(); + } + + /** + * Return the value that failed to be generated. + * @return the value + */ + @Nullable + public Object getValue() { + return this.value; + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java new file mode 100644 index 000000000000..61008f5aa259 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Code generator for a single value. Delegates code generation to a list of + * configurable {@link Delegate} implementations. + * + * @author Stephane Nicoll + * @since 6.1.2 + */ +public final class ValueCodeGenerator { + + private static final ValueCodeGenerator INSTANCE = new ValueCodeGenerator(ValueCodeGeneratorDelegates.INSTANCES, null); + + private static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null"); + + private final List delegates; + + @Nullable + private final GeneratedMethods generatedMethods; + + private ValueCodeGenerator(List delegates, @Nullable GeneratedMethods generatedMethods) { + this.delegates = delegates; + this.generatedMethods = generatedMethods; + } + + /** + * Return an instance that provides support for {@linkplain + * ValueCodeGeneratorDelegates#INSTANCES common value types}. + * @return an instance with support for common value types + */ + public static ValueCodeGenerator withDefaults() { + return INSTANCE; + } + + /** + * Create an instance with the specified {@link Delegate} implementations. + * @param delegates the delegates to use + * @return an instance with the specified delegates + */ + public static ValueCodeGenerator with(Delegate... delegates) { + return with(Arrays.asList(delegates)); + } + + /** + * Create an instance with the specified {@link Delegate} implementations. + * @param delegates the delegates to use + * @return an instance with the specified delegates + */ + public static ValueCodeGenerator with(List delegates) { + Assert.notEmpty(delegates, "Delegates must not be empty"); + return new ValueCodeGenerator(new ArrayList<>(delegates), null); + } + + public ValueCodeGenerator add(List additionalDelegates) { + Assert.notEmpty(additionalDelegates, "AdditionalDelegates must not be empty"); + List allDelegates = new ArrayList<>(this.delegates); + allDelegates.addAll(additionalDelegates); + return new ValueCodeGenerator(allDelegates, this.generatedMethods); + } + + /** + * Return a {@link ValueCodeGenerator} that is scoped for the specified + * {@link GeneratedMethods}. This allows code generation to generate + * additional methods if necessary, or perform some optimization in + * case of visibility issues. + * @param generatedMethods the generated methods to use + * @return an instance scoped to the specified generated methods + */ + public ValueCodeGenerator scoped(GeneratedMethods generatedMethods) { + return new ValueCodeGenerator(this.delegates, generatedMethods); + } + + /** + * Generate the code that represents the specified {@code value}. + * @param value the value to generate + * @return the code that represents the specified value + */ + public CodeBlock generateCode(@Nullable Object value) { + if (value == null) { + return NULL_VALUE_CODE_BLOCK; + } + try { + for (Delegate delegate : this.delegates) { + CodeBlock code = delegate.generateCode(this, value); + if (code != null) { + return code; + } + } + throw new UnsupportedTypeValueCodeGenerationException(value); + } + catch (Exception ex) { + throw new ValueCodeGenerationException(value, ex); + } + } + + + /** + * Return the {@link GeneratedMethods} that represents the scope + * in which code generated by this instance will be added, or + * {@code null} if no specific scope is set. + * @return the generated methods to use for code generation + */ + @Nullable + public GeneratedMethods getGeneratedMethods() { + return this.generatedMethods; + } + + /** + * Strategy interface that can be used to implement code generation for a + * particular value type. + */ + public interface Delegate { + + /** + * Generate the code for the specified non-null {@code value}. If this + * instance does not support the value, it should return {@code null} to + * indicate so. + * @param valueCodeGenerator the code generator to use for embedded values + * @param value the value to generate + * @return the code that represents the specified value or {@code null} if + * the specified value is not supported. + */ + @Nullable + CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value); + + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java new file mode 100644 index 000000000000..cd59c2ccf59e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java @@ -0,0 +1,420 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; + +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Code generator {@link Delegate} for well known value types. + * + * @author Stephane Nicoll + * @since 6.1.2 + */ +public abstract class ValueCodeGeneratorDelegates { + + /** + * Return the {@link Delegate} implementations for common value types. + * These are: + *

    + *
  • Primitive types,
  • + *
  • String,
  • + *
  • Charset,
  • + *
  • Enum,
  • + *
  • Class,
  • + *
  • {@link ResolvableType},
  • + *
  • Array,
  • + *
  • List via {@code List.of},
  • + *
  • Set via {@code Set.of} and support of {@link LinkedHashSet},
  • + *
  • Map via {@code Map.of} or {@code Map.ofEntries}.
  • + *
+ * Those implementations do not require the {@link ValueCodeGenerator} to be + * {@linkplain ValueCodeGenerator#scoped(GeneratedMethods) scoped}. + */ + public static final List INSTANCES = List.of( + new PrimitiveDelegate(), + new StringDelegate(), + new CharsetDelegate(), + new EnumDelegate(), + new ClassDelegate(), + new ResolvableTypeDelegate(), + new ArrayDelegate(), + new ListDelegate(), + new SetDelegate(), + new MapDelegate()); + + + /** + * Abstract {@link Delegate} for {@code Collection} types. + * @param type the collection type + */ + public abstract static class CollectionDelegate> implements Delegate { + + private final Class collectionType; + + private final CodeBlock emptyResult; + + protected CollectionDelegate(Class collectionType, CodeBlock emptyResult) { + this.collectionType = collectionType; + this.emptyResult = emptyResult; + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (this.collectionType.isInstance(value)) { + T collection = (T) value; + if (collection.isEmpty()) { + return this.emptyResult; + } + return generateCollectionCode(valueCodeGenerator, collection); + } + return null; + } + + protected CodeBlock generateCollectionCode(ValueCodeGenerator valueCodeGenerator, T collection) { + return generateCollectionOf(valueCodeGenerator, collection, this.collectionType); + } + + protected final CodeBlock generateCollectionOf(ValueCodeGenerator valueCodeGenerator, + Collection collection, Class collectionType) { + Builder code = CodeBlock.builder(); + code.add("$T.of(", collectionType); + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + Object element = iterator.next(); + code.add("$L", valueCodeGenerator.generateCode(element)); + if (iterator.hasNext()) { + code.add(", "); + } + } + code.add(")"); + return code.build(); + } + } + + + /** + * {@link Delegate} for {@link Map} types. + */ + public static class MapDelegate implements Delegate { + + private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class); + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof Map map) { + if (map.isEmpty()) { + return EMPTY_RESULT; + } + return generateMapCode(valueCodeGenerator, map); + } + return null; + } + + /** + * Generate the code for a non-empty {@link Map}. + * @param valueCodeGenerator the code generator to use for embedded values + * @param map the value to generate + * @return the code that represents the specified map or {@code null} if + * the specified map is not supported. + */ + @Nullable + protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { + map = orderForCodeConsistency(map); + boolean useOfEntries = map.size() > 10; + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T" + ((!useOfEntries) ? ".of(" : ".ofEntries("), Map.class); + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + CodeBlock keyCode = valueCodeGenerator.generateCode(entry.getKey()); + CodeBlock valueCode = valueCodeGenerator.generateCode(entry.getValue()); + if (!useOfEntries) { + code.add("$L, $L", keyCode, valueCode); + } + else { + code.add("$T.entry($L,$L)", Map.class, keyCode, valueCode); + } + if (iterator.hasNext()) { + code.add(", "); + } + } + code.add(")"); + return code.build(); + } + + private Map orderForCodeConsistency(Map map) { + try { + return new TreeMap<>(map); + } + catch (ClassCastException ex) { + // If elements are not comparable, just keep the original map + return map; + } + } + } + + + /** + * {@link Delegate} for {@code primitive} types. + */ + private static class PrimitiveDelegate implements Delegate { + + private static final Map CHAR_ESCAPES = Map.of( + '\b', "\\b", + '\t', "\\t", + '\n', "\\n", + '\f', "\\f", + '\r', "\\r", + '\"', "\"", + '\'', "\\'", + '\\', "\\\\" + ); + + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + if (value instanceof Boolean || value instanceof Integer) { + return CodeBlock.of("$L", value); + } + if (value instanceof Byte) { + return CodeBlock.of("(byte) $L", value); + } + if (value instanceof Short) { + return CodeBlock.of("(short) $L", value); + } + if (value instanceof Long) { + return CodeBlock.of("$LL", value); + } + if (value instanceof Float) { + return CodeBlock.of("$LF", value); + } + if (value instanceof Double) { + return CodeBlock.of("(double) $L", value); + } + if (value instanceof Character character) { + return CodeBlock.of("'$L'", escape(character)); + } + return null; + } + + private String escape(char ch) { + String escaped = CHAR_ESCAPES.get(ch); + if (escaped != null) { + return escaped; + } + return (!Character.isISOControl(ch)) ? Character.toString(ch) + : String.format("\\u%04x", (int) ch); + } + } + + + /** + * {@link Delegate} for {@link String} types. + */ + private static class StringDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + if (value instanceof String) { + return CodeBlock.of("$S", value); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link Charset} types. + */ + private static class CharsetDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + if (value instanceof Charset charset) { + return CodeBlock.of("$T.forName($S)", Charset.class, charset.name()); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link Enum} types. + */ + private static class EnumDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + if (value instanceof Enum enumValue) { + return CodeBlock.of("$T.$L", enumValue.getDeclaringClass(), + enumValue.name()); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link Class} types. + */ + private static class ClassDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + if (value instanceof Class clazz) { + return CodeBlock.of("$T.class", ClassUtils.getUserClass(clazz)); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link ResolvableType} types. + */ + private static class ResolvableTypeDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + if (value instanceof ResolvableType resolvableType) { + return generateCode(resolvableType, false); + } + return null; + } + + + private static CodeBlock generateCode(ResolvableType resolvableType, boolean allowClassResult) { + if (ResolvableType.NONE.equals(resolvableType)) { + return CodeBlock.of("$T.NONE", ResolvableType.class); + } + Class type = ClassUtils.getUserClass(resolvableType.toClass()); + if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) { + return generateCodeWithGenerics(resolvableType, type); + } + if (allowClassResult) { + return CodeBlock.of("$T.class", type); + } + return CodeBlock.of("$T.forClass($T.class)", ResolvableType.class, type); + } + + private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class type) { + ResolvableType[] generics = target.getGenerics(); + boolean hasNoNestedGenerics = Arrays.stream(generics).noneMatch(ResolvableType::hasGenerics); + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.forClassWithGenerics($T.class", ResolvableType.class, type); + for (ResolvableType generic : generics) { + code.add(", $L", generateCode(generic, hasNoNestedGenerics)); + } + code.add(")"); + return code.build(); + } + } + + + /** + * {@link Delegate} for {@code array} types. + */ + private static class ArrayDelegate implements Delegate { + + @Override + @Nullable + public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + if (value.getClass().isArray()) { + Stream elements = Arrays.stream(ObjectUtils.toObjectArray(value)) + .map(codeGenerator::generateCode); + CodeBlock.Builder code = CodeBlock.builder(); + code.add("new $T {", value.getClass()); + code.add(elements.collect(CodeBlock.joining(", "))); + code.add("}"); + return code.build(); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link List} types. + */ + private static class ListDelegate extends CollectionDelegate> { + + ListDelegate() { + super(List.class, CodeBlock.of("$T.emptyList()", Collections.class)); + } + } + + + /** + * {@link Delegate} for {@link Set} types. + */ + private static class SetDelegate extends CollectionDelegate> { + + SetDelegate() { + super(Set.class, CodeBlock.of("$T.emptySet()", Collections.class)); + } + + @Override + protected CodeBlock generateCollectionCode(ValueCodeGenerator valueCodeGenerator, Set collection) { + if (collection instanceof LinkedHashSet) { + return CodeBlock.of("new $T($L)", LinkedHashSet.class, + generateCollectionOf(valueCodeGenerator, collection, List.class)); + } + return super.generateCollectionCode(valueCodeGenerator, + orderForCodeConsistency(collection)); + } + + private Set orderForCodeConsistency(Set set) { + try { + return new TreeSet(set); + } + catch (ClassCastException ex) { + // If elements are not comparable, just keep the original set + return set; + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java index 874ce2ad67b8..77c78341674f 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java @@ -79,6 +79,10 @@ protected String addPackageIfNecessary(String part) { protected abstract boolean isPrimitive(); + @Override + public int compareTo(TypeReference other) { + return this.getCanonicalName().compareToIgnoreCase(other.getCanonicalName()); + } @Override public boolean equals(@Nullable Object other) { diff --git a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java index ac0375f2dd13..740ae87f10d4 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ /** * Register the necessary reflection hints so that the specified type can be - * bound at runtime. Fields, constructors, properties and record components + * bound at runtime. Fields, constructors, properties, and record components * are registered, except for a set of types like those in the {@code java.} * package where just the type is registered. Types are discovered transitively * on properties and record components, and generic types are registered as well. @@ -54,8 +54,9 @@ public class BindingReflectionHintsRegistrar { private static final String JACKSON_ANNOTATION = "com.fasterxml.jackson.annotation.JacksonAnnotation"; - private static final boolean jacksonAnnotationPresent = ClassUtils.isPresent(JACKSON_ANNOTATION, - BindingReflectionHintsRegistrar.class.getClassLoader()); + private static final boolean jacksonAnnotationPresent = + ClassUtils.isPresent(JACKSON_ANNOTATION, BindingReflectionHintsRegistrar.class.getClassLoader()); + /** * Register the necessary reflection hints to bind the specified types. @@ -94,16 +95,20 @@ private void registerReflectionHints(ReflectionHints hints, Set seen, Type registerRecordHints(hints, seen, recordComponent.getAccessor()); } } - typeHint.withMembers( - MemberCategory.DECLARED_FIELDS, + if (clazz.isEnum()) { + typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS); + } + typeHint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); for (Method method : clazz.getMethods()) { String methodName = method.getName(); if (methodName.startsWith("set") && method.getParameterCount() == 1) { registerPropertyHints(hints, seen, method, 0); } - else if ((methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) || - (methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class)) { + else if ((methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) || + (methodName.startsWith("is") && method.getParameterCount() == 0 + && ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()) == Boolean.class)) { registerPropertyHints(hints, seen, method, -1); } } @@ -132,8 +137,7 @@ private void registerRecordHints(ReflectionHints hints, Set seen, Method m } private void registerPropertyHints(ReflectionHints hints, Set seen, @Nullable Method method, int parameterIndex) { - if (method != null && method.getDeclaringClass() != Object.class && - method.getDeclaringClass() != Enum.class) { + if (method != null && method.getDeclaringClass() != Object.class && method.getDeclaringClass() != Enum.class) { hints.registerMethod(method, ExecutableMode.INVOKE); MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex); Type methodParameterType = methodParameter.getGenericParameterType(); @@ -191,17 +195,24 @@ private void forEachJacksonAnnotation(AnnotatedElement element, Consumer annotation) { - annotation.getRoot().asMap().values().forEach(value -> { + annotation.getRoot().asMap().forEach((attributeName, value) -> { if (value instanceof Class classValue && value != Void.class) { - hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + if (attributeName.equals("builder")) { + hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS); + } + else { + hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } } }); } + /** * Inner class to avoid a hard dependency on Kotlin at runtime. */ diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java index 3cf8fd7474a5..5d0c3a4d26dc 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,10 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.util.Comparator; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -32,7 +34,7 @@ * @author Stephane Nicoll * @since 6.0 */ -public final class ExecutableHint extends MemberHint { +public final class ExecutableHint extends MemberHint implements Comparable { private final List parameterTypes; @@ -91,6 +93,17 @@ public static Consumer builtWith(ExecutableMode mode) { return builder -> builder.withMode(mode); } + @Override + public int compareTo(ExecutableHint other) { + return Comparator.comparing(ExecutableHint::getName, String::compareToIgnoreCase) + .thenComparing(ExecutableHint::getParameterTypes, Comparator.comparingInt(List::size)) + .thenComparing(ExecutableHint::getParameterTypes, (params1, params2) -> { + String left = params1.stream().map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + String right = params2.stream().map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + return left.compareTo(right); + }).compare(this, other); + } + /** * Builder for {@link ExecutableHint}. */ diff --git a/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java b/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java index 9424b17f9afa..2d09bb0e4594 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java @@ -124,6 +124,6 @@ public enum MemberCategory { * reflection for inner classes but rather makes sure they are available * via a call to {@link Class#getDeclaredClasses}. */ - DECLARED_CLASSES; + DECLARED_CLASSES } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java index 310f29512d22..3e4a26557a9e 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java @@ -37,7 +37,7 @@ private ReflectionTypeReference(Class type) { @Nullable private static TypeReference getEnclosingClass(Class type) { - Class candidate = (type.isArray() ? type.getComponentType().getEnclosingClass() : + Class candidate = (type.isArray() ? type.componentType().getEnclosingClass() : type.getEnclosingClass()); return (candidate != null ? new ReflectionTypeReference(candidate) : null); } @@ -56,7 +56,7 @@ public String getCanonicalName() { @Override protected boolean isPrimitive() { return this.type.isPrimitive() || - (this.type.isArray() && this.type.getComponentType().isPrimitive()); + (this.type.isArray() && this.type.componentType().isPrimitive()); } } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java index 483db7d9e089..490dd257f497 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ public Stream resourceBundleHints() { */ public ResourceHints registerPatternIfPresent(@Nullable ClassLoader classLoader, String location, Consumer resourceHint) { - ClassLoader classLoaderToUse = (classLoader != null) ? classLoader : getClass().getClassLoader(); + ClassLoader classLoaderToUse = (classLoader != null ? classLoader : getClass().getClassLoader()); if (classLoaderToUse.getResource(location) != null) { registerPattern(resourceHint); } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java index 876f6a3fe9f0..78c8ce9a6e55 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -143,7 +143,7 @@ public Builder includes(String... includes) { * @param excludes the exclude patterns (see {@link ResourcePatternHint} documentation) * @return {@code this}, to facilitate method chaining */ - public Builder excludes(TypeReference reachableType, String... excludes) { + public Builder excludes(@Nullable TypeReference reachableType, String... excludes) { List newExcludes = Arrays.stream(excludes) .map(include -> new ResourcePatternHint(include, reachableType)).toList(); this.excludes.addAll(newExcludes); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java index b21df02e8245..62a3ff9978cf 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ private static SimpleTypeReference createTypeReference(String className) { return new SimpleTypeReference(className.substring(0, i), className.substring(i + 1), null); } else { - String packageName = isPrimitive(className) ? "java.lang" : ""; + String packageName = (isPrimitive(className) ? "java.lang" : ""); return new SimpleTypeReference(packageName, className, null); } } @@ -101,7 +101,7 @@ private static void buildName(@Nullable TypeReference type, StringBuilder sb) { if (type == null) { return; } - String typeName = (type.getEnclosingType() != null) ? "." + type.getSimpleName() : type.getSimpleName(); + String typeName = (type.getEnclosingType() != null ? "." + type.getSimpleName() : type.getSimpleName()); sb.insert(0, typeName); buildName(type.getEnclosingType(), sb); } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java index 6e222de0be11..6bc2c8cf2886 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java @@ -29,7 +29,7 @@ * @author Sebastien Deleuze * @since 6.0 */ -public interface TypeReference { +public interface TypeReference extends Comparable { /** * Return the fully qualified name of this type reference. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java index e12ceae9d490..02dcdcef6ba4 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,8 +39,7 @@ * @see ReflectiveRuntimeHintsRegistrar * @see RegisterReflectionForBinding @RegisterReflectionForBinding */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR, - ElementType.FIELD, ElementType.METHOD }) +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Reflective { diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java index d0a5b9c98ba9..ea0077c9eb6a 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,13 +36,12 @@ * *
  * @Configuration
- * @RegisterReflectionForBinding({ Foo.class, Bar.class })
+ * @RegisterReflectionForBinding({Foo.class, Bar.class})
  * public class MyConfig {
  *     // ...
  * }
* - *

The annotated element can be any Spring bean class, constructor, field, - * or method — for example: + *

The annotated element can be any Spring bean class or method — for example: * *

  * @Service
@@ -63,7 +62,7 @@
  * @see org.springframework.aot.hint.BindingReflectionHintsRegistrar
  * @see Reflective @Reflective
  */
-@Target({ ElementType.TYPE, ElementType.METHOD })
+@Target({ElementType.TYPE, ElementType.METHOD})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Reflective(RegisterReflectionForBindingProcessor.class)
@@ -77,8 +76,7 @@
 
 	/**
 	 * Classes for which reflection hints should be registered.
-	 * 

At least one class must be specified either via {@link #value} or - * {@link #classes}. + *

At least one class must be specified either via {@link #value} or {@code classes}. * @see #value() */ @AliasFor("value") diff --git a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java index 01d25229a0b5..a9f87de77ac1 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -194,6 +194,7 @@ public FieldHintPredicate onField(Field field) { return new FieldHintPredicate(field); } + public static class TypeHintPredicate implements Predicate { private final TypeReference type; @@ -212,7 +213,6 @@ public boolean test(RuntimeHints hints) { return getTypeHint(hints) != null; } - /** * Refine the current predicate to only match if the given {@link MemberCategory} is present. * @param memberCategory the member category @@ -220,7 +220,10 @@ public boolean test(RuntimeHints hints) { */ public Predicate withMemberCategory(MemberCategory memberCategory) { Assert.notNull(memberCategory, "'memberCategory' must not be null"); - return this.and(hints -> getTypeHint(hints).getMemberCategories().contains(memberCategory)); + return and(hints -> { + TypeHint hint = getTypeHint(hints); + return (hint != null && hint.getMemberCategories().contains(memberCategory)); + }); } /** @@ -230,7 +233,10 @@ public Predicate withMemberCategory(MemberCategory memberCategory) */ public Predicate withMemberCategories(MemberCategory... memberCategories) { Assert.notEmpty(memberCategories, "'memberCategories' must not be empty"); - return this.and(hints -> getTypeHint(hints).getMemberCategories().containsAll(Arrays.asList(memberCategories))); + return and(hints -> { + TypeHint hint = getTypeHint(hints); + return (hint != null && hint.getMemberCategories().containsAll(Arrays.asList(memberCategories))); + }); } /** @@ -240,12 +246,15 @@ public Predicate withMemberCategories(MemberCategory... memberCate */ public Predicate withAnyMemberCategory(MemberCategory... memberCategories) { Assert.notEmpty(memberCategories, "'memberCategories' must not be empty"); - return this.and(hints -> Arrays.stream(memberCategories) - .anyMatch(memberCategory -> getTypeHint(hints).getMemberCategories().contains(memberCategory))); + return and(hints -> { + TypeHint hint = getTypeHint(hints); + return (hint != null && Arrays.stream(memberCategories) + .anyMatch(memberCategory -> hint.getMemberCategories().contains(memberCategory))); + }); } - } + public abstract static class ExecutableHintPredicate implements Predicate { protected final T executable; @@ -289,6 +298,7 @@ static boolean includes(ExecutableHint hint, String name, } } + public static class ConstructorHintPredicate extends ExecutableHintPredicate> { ConstructorHintPredicate(Constructor constructor) { @@ -322,15 +332,17 @@ MemberCategory[] getDeclaredMemberCategories() { @Override Predicate exactMatch() { - return hints -> (hints.reflection().getTypeHint(this.executable.getDeclaringClass()) != null) && - hints.reflection().getTypeHint(this.executable.getDeclaringClass()).constructors().anyMatch(executableHint -> { - List parameters = TypeReference.listOf(this.executable.getParameterTypes()); - return includes(executableHint, "", parameters, this.executableMode); - }); + return hints -> { + TypeHint hint = hints.reflection().getTypeHint(this.executable.getDeclaringClass()); + return (hint != null && hint.constructors().anyMatch(executableHint -> { + List parameters = TypeReference.listOf(this.executable.getParameterTypes()); + return includes(executableHint, "", parameters, this.executableMode); + })); + }; } - } + public static class MethodHintPredicate extends ExecutableHintPredicate { MethodHintPredicate(Method method) { @@ -367,15 +379,17 @@ MemberCategory[] getDeclaredMemberCategories() { @Override Predicate exactMatch() { - return hints -> (hints.reflection().getTypeHint(this.executable.getDeclaringClass()) != null) && - hints.reflection().getTypeHint(this.executable.getDeclaringClass()).methods().anyMatch(executableHint -> { - List parameters = TypeReference.listOf(this.executable.getParameterTypes()); - return includes(executableHint, this.executable.getName(), parameters, this.executableMode); - }); + return hints -> { + TypeHint hint = hints.reflection().getTypeHint(this.executable.getDeclaringClass()); + return (hint != null && hint.methods().anyMatch(executableHint -> { + List parameters = TypeReference.listOf(this.executable.getParameterTypes()); + return includes(executableHint, this.executable.getName(), parameters, this.executableMode); + })); + }; } - } + public static class FieldHintPredicate implements Predicate { private final Field field; @@ -406,7 +420,6 @@ private boolean exactMatch(TypeHint typeHint) { return typeHint.fields().anyMatch(fieldHint -> this.field.getName().equals(fieldHint.getName())); } - } } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHints.java new file mode 100644 index 000000000000..ed820c4a1532 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHints.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.hint.support; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.Nullable; + +/** + * {@link RuntimeHintsRegistrar} to register hints for {@link org.springframework.core.KotlinDetector}. + * + * @author Brian Clozel + * @since 6.1 + */ +class KotlinDetectorRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + hints.reflection().registerType(TypeReference.of("kotlin.Metadata")) + .registerType(TypeReference.of("kotlin.reflect.full.KClasses")); + } +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHints.java new file mode 100644 index 000000000000..371812e06c5b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHints.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.hint.support; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.lang.Nullable; + +/** + * {@link RuntimeHintsRegistrar} for {@link PathMatchingResourcePatternResolver}. + * @author Brian Clozel + */ +class PathMatchingResourcePatternResolverRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + hints.reflection().registerType(TypeReference.of("org.eclipse.core.runtime.FileLocator")); + } +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java index 9d8318e438da..8081223c5215 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,9 +47,11 @@ class SpringFactoriesLoaderRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + ClassLoader classLoaderToUse = (classLoader != null ? classLoader : + SpringFactoriesLoaderRuntimeHints.class.getClassLoader()); for (String resourceLocation : RESOURCE_LOCATIONS) { - registerHints(hints, classLoader, resourceLocation); + registerHints(hints, classLoaderToUse, resourceLocation); } } @@ -63,6 +65,7 @@ private void registerHints(RuntimeHints hints, ClassLoader classLoader, String r private void registerHints(RuntimeHints hints, ClassLoader classLoader, String factoryClassName, List implementationClassNames) { + Class factoryClass = resolveClassName(classLoader, factoryClassName); if (factoryClass == null) { if (logger.isTraceEnabled()) { @@ -100,6 +103,7 @@ private Class resolveClassName(ClassLoader classLoader, String factoryClassNa } } + private static class ExtendedSpringFactoriesLoader extends SpringFactoriesLoader { ExtendedSpringFactoriesLoader(@Nullable ClassLoader classLoader, Map> factories) { @@ -109,7 +113,6 @@ private static class ExtendedSpringFactoriesLoader extends SpringFactoriesLoader static Map> accessLoadFactoriesResource(ClassLoader classLoader, String resourceLocation) { return SpringFactoriesLoader.loadFactoriesResource(classLoader, resourceLocation); } - } } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHints.java new file mode 100644 index 000000000000..50e61caa4dae --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHints.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.hint.support; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.lang.Nullable; + +/** + * {@link RuntimeHintsRegistrar} to register hints for {@link org.springframework.core.SpringProperties}. + * + * @author Brian Clozel + * @since 6.1 + */ +class SpringPropertiesRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + hints.resources().registerPattern("spring.properties"); + } +} diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java index 055a0d1160a9..51cd3132efac 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,14 @@ package org.springframework.aot.nativex; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Collectors; import org.springframework.aot.hint.JdkProxyHint; import org.springframework.aot.hint.ProxyHints; +import org.springframework.aot.hint.TypeReference; /** * Write {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON @@ -38,8 +41,18 @@ class ProxyHintsWriter { public static final ProxyHintsWriter INSTANCE = new ProxyHintsWriter(); + private static final Comparator JDK_PROXY_HINT_COMPARATOR = + (left, right) -> { + String leftSignature = left.getProxiedInterfaces().stream() + .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + String rightSignature = right.getProxiedInterfaces().stream() + .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + return leftSignature.compareTo(rightSignature); + }; + public void write(BasicJsonWriter writer, ProxyHints hints) { - writer.writeArray(hints.jdkProxyHints().map(this::toAttributes).toList()); + writer.writeArray(hints.jdkProxyHints().sorted(JDK_PROXY_HINT_COMPARATOR) + .map(this::toAttributes).toList()); } private Map toAttributes(JdkProxyHint hint) { diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java index 8a2b3603b050..3d678b0d85cf 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.aot.nativex; import java.util.Collection; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -49,7 +50,9 @@ class ReflectionHintsWriter { public static final ReflectionHintsWriter INSTANCE = new ReflectionHintsWriter(); public void write(BasicJsonWriter writer, ReflectionHints hints) { - writer.writeArray(hints.typeHints().map(this::toAttributes).toList()); + writer.writeArray(hints.typeHints() + .sorted(Comparator.comparing(TypeHint::getType)) + .map(this::toAttributes).toList()); } private Map toAttributes(TypeHint hint) { @@ -58,7 +61,8 @@ private Map toAttributes(TypeHint hint) { handleCondition(attributes, hint); handleCategories(attributes, hint.getMemberCategories()); handleFields(attributes, hint.fields()); - handleExecutables(attributes, Stream.concat(hint.constructors(), hint.methods()).toList()); + handleExecutables(attributes, Stream.concat( + hint.constructors(), hint.methods()).sorted().toList()); return attributes; } @@ -71,7 +75,9 @@ private void handleCondition(Map attributes, TypeHint hint) { } private void handleFields(Map attributes, Stream fields) { - addIfNotEmpty(attributes, "fields", fields.map(this::toAttributes).toList()); + addIfNotEmpty(attributes, "fields", fields + .sorted(Comparator.comparing(FieldHint::getName, String::compareToIgnoreCase)) + .map(this::toAttributes).toList()); } private Map toAttributes(FieldHint hint) { @@ -97,7 +103,7 @@ private Map toAttributes(ExecutableHint hint) { } private void handleCategories(Map attributes, Set categories) { - categories.forEach(category -> { + categories.stream().sorted().forEach(category -> { switch (category) { case PUBLIC_FIELDS -> attributes.put("allPublicFields", true); case DECLARED_FIELDS -> attributes.put("allDeclaredFields", true); diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java index cdbaba7884bf..6829006e9029 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.aot.nativex; import java.util.Collection; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -44,6 +45,13 @@ class ResourceHintsWriter { public static final ResourceHintsWriter INSTANCE = new ResourceHintsWriter(); + private static final Comparator RESOURCE_PATTERN_HINT_COMPARATOR = + Comparator.comparing(ResourcePatternHint::getPattern); + + private static final Comparator RESOURCE_BUNDLE_HINT_COMPARATOR = + Comparator.comparing(ResourceBundleHint::getBaseName); + + public void write(BasicJsonWriter writer, ResourceHints hints) { Map attributes = new LinkedHashMap<>(); addIfNotEmpty(attributes, "resources", toAttributes(hints)); @@ -53,15 +61,21 @@ public void write(BasicJsonWriter writer, ResourceHints hints) { private Map toAttributes(ResourceHints hint) { Map attributes = new LinkedHashMap<>(); - addIfNotEmpty(attributes, "includes", hint.resourcePatternHints().map(ResourcePatternHints::getIncludes) - .flatMap(List::stream).distinct().map(this::toAttributes).toList()); - addIfNotEmpty(attributes, "excludes", hint.resourcePatternHints().map(ResourcePatternHints::getExcludes) - .flatMap(List::stream).distinct().map(this::toAttributes).toList()); + addIfNotEmpty(attributes, "includes", hint.resourcePatternHints() + .map(ResourcePatternHints::getIncludes).flatMap(List::stream).distinct() + .sorted(RESOURCE_PATTERN_HINT_COMPARATOR) + .map(this::toAttributes).toList()); + addIfNotEmpty(attributes, "excludes", hint.resourcePatternHints() + .map(ResourcePatternHints::getExcludes).flatMap(List::stream).distinct() + .sorted(RESOURCE_PATTERN_HINT_COMPARATOR) + .map(this::toAttributes).toList()); return attributes; } - private void handleResourceBundles(Map attributes, Stream ressourceBundles) { - addIfNotEmpty(attributes, "bundles", ressourceBundles.map(this::toAttributes).toList()); + private void handleResourceBundles(Map attributes, Stream resourceBundles) { + addIfNotEmpty(attributes, "bundles", resourceBundles + .sorted(RESOURCE_BUNDLE_HINT_COMPARATOR) + .map(this::toAttributes).toList()); } private Map toAttributes(ResourceBundleHint hint) { diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java index cd0f6c802dcd..73b25248519e 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.aot.nativex; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; @@ -38,8 +39,13 @@ class SerializationHintsWriter { public static final SerializationHintsWriter INSTANCE = new SerializationHintsWriter(); + private static final Comparator JAVA_SERIALIZATION_HINT_COMPARATOR = + Comparator.comparing(JavaSerializationHint::getType); + public void write(BasicJsonWriter writer, SerializationHints hints) { - writer.writeArray(hints.javaSerializationHints().map(this::toAttributes).toList()); + writer.writeArray(hints.javaSerializationHints() + .sorted(JAVA_SERIALIZATION_HINT_COMPARATOR) + .map(this::toAttributes).toList()); } private Map toAttributes(JavaSerializationHint serializationHint) { diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/feature/PreComputeFieldFeature.java b/spring-core/src/main/java/org/springframework/aot/nativex/feature/PreComputeFieldFeature.java index aa9d524238f6..d0ca2c7146dc 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/feature/PreComputeFieldFeature.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/feature/PreComputeFieldFeature.java @@ -26,13 +26,20 @@ * GraalVM {@link Feature} that substitutes boolean field values that match a certain pattern * with values pre-computed AOT without causing class build-time initialization. * + *

It is possible to pass

-Dspring.native.precompute.log=verbose
as a + *
native-image
compiler build argument to display detailed logs + * about pre-computed fields.

+ * * @author Sebastien Deleuze * @author Phillip Webb * @since 6.0 */ class PreComputeFieldFeature implements Feature { - private static Pattern[] patterns = { + private static final boolean verbose = + "verbose".equalsIgnoreCase(System.getProperty("spring.native.precompute.log")); + + private static final Pattern[] patterns = { Pattern.compile(Pattern.quote("org.springframework.core.NativeDetector#inNativeImage")), Pattern.compile(Pattern.quote("org.springframework.cglib.core.AbstractClassGenerator#inNativeImage")), Pattern.compile(Pattern.quote("org.springframework.aot.AotDetector#inNativeImage")), @@ -42,14 +49,15 @@ class PreComputeFieldFeature implements Feature { Pattern.compile(Pattern.quote("org.apache.commons.logging.LogAdapter") + "#.*Present") }; - private final ThrowawayClassLoader throwawayClassLoader = new ThrowawayClassLoader(PreComputeFieldFeature.class.getClassLoader()); + private final ThrowawayClassLoader throwawayClassLoader = new ThrowawayClassLoader(getClass().getClassLoader()); + @Override public void beforeAnalysis(BeforeAnalysisAccess access) { access.registerSubtypeReachabilityHandler(this::iterateFields, Object.class); } - /* This method is invoked for every type that is reachable. */ + // This method is invoked for every type that is reachable. private void iterateFields(DuringAnalysisAccess access, Class subtype) { try { for (Field field : subtype.getDeclaredFields()) { @@ -64,10 +72,16 @@ private void iterateFields(DuringAnalysisAccess access, Class subtype) { try { Object fieldValue = provideFieldValue(field); access.registerFieldValueTransformer(field, (receiver, originalValue) -> fieldValue); - System.out.println("Field " + fieldIdentifier + " set to " + fieldValue + " at build time"); + if (verbose) { + System.out.println( + "Field " + fieldIdentifier + " set to " + fieldValue + " at build time"); + } } catch (Throwable ex) { - System.out.println("Field " + fieldIdentifier + " will be evaluated at runtime due to this error during build time evaluation: " + ex.getMessage()); + if (verbose) { + System.out.println("Field " + fieldIdentifier + " will be evaluated at runtime " + + "due to this error during build time evaluation: " + ex); + } } } } @@ -78,8 +92,10 @@ private void iterateFields(DuringAnalysisAccess access, Class subtype) { } } - /* This method is invoked when the field value is written to the image heap or the field is constant folded. */ - private Object provideFieldValue(Field field) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + // This method is invoked when the field value is written to the image heap or the field is constant folded. + private Object provideFieldValue(Field field) + throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + Class throwawayClass = this.throwawayClassLoader.loadClass(field.getDeclaringClass().getName()); Field throwawayField = throwawayClass.getDeclaredField(field.getName()); throwawayField.setAccessible(true); diff --git a/spring-core/src/main/java/org/springframework/asm/AnnotationWriter.java b/spring-core/src/main/java/org/springframework/asm/AnnotationWriter.java index 6494a79c1cbf..f3826da46778 100644 --- a/spring-core/src/main/java/org/springframework/asm/AnnotationWriter.java +++ b/spring-core/src/main/java/org/springframework/asm/AnnotationWriter.java @@ -144,7 +144,7 @@ static AnnotationWriter create( // Write type_index and reserve space for num_element_value_pairs. annotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); return new AnnotationWriter( - symbolTable, /* useNamedValues = */ true, annotation, previousAnnotation); + symbolTable, /* useNamedValues= */ true, annotation, previousAnnotation); } /** @@ -179,7 +179,7 @@ static AnnotationWriter create( // Write type_index and reserve space for num_element_value_pairs. typeAnnotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); return new AnnotationWriter( - symbolTable, /* useNamedValues = */ true, typeAnnotation, previousAnnotation); + symbolTable, /* useNamedValues= */ true, typeAnnotation, previousAnnotation); } // ----------------------------------------------------------------------------------------------- @@ -284,7 +284,7 @@ public AnnotationVisitor visitAnnotation(final String name, final String descrip } // Write tag and type_index, and reserve 2 bytes for num_element_value_pairs. annotation.put12('@', symbolTable.addConstantUtf8(descriptor)).putShort(0); - return new AnnotationWriter(symbolTable, /* useNamedValues = */ true, annotation, null); + return new AnnotationWriter(symbolTable, /* useNamedValues= */ true, annotation, null); } @Override @@ -303,7 +303,7 @@ public AnnotationVisitor visitArray(final String name) { // visit the array elements. Its num_element_value_pairs will correspond to the number of array // elements and will be stored in what is in fact num_values. annotation.put12('[', 0); - return new AnnotationWriter(symbolTable, /* useNamedValues = */ false, annotation, null); + return new AnnotationWriter(symbolTable, /* useNamedValues= */ false, annotation, null); } @Override diff --git a/spring-core/src/main/java/org/springframework/asm/ClassReader.java b/spring-core/src/main/java/org/springframework/asm/ClassReader.java index d84485cafdf5..64acfe84b82d 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassReader.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassReader.java @@ -177,7 +177,7 @@ public ClassReader( final byte[] classFileBuffer, final int classFileOffset, final int classFileLength) { // NOPMD(UnusedFormalParameter) used for backward compatibility. - this(classFileBuffer, classFileOffset, /* checkClassVersion = */ true); + this(classFileBuffer, classFileOffset, /* checkClassVersion= */ true); } /** @@ -194,7 +194,7 @@ public ClassReader( this.b = classFileBuffer; // Check the class' major_version. This field is after the magic and minor_version fields, which // use 4 and 2 bytes respectively. - if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V21) { + if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V23) { throw new IllegalArgumentException( "Unsupported class file major version " + readShort(classFileOffset + 6)); } @@ -608,9 +608,9 @@ public void accept( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - classVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + classVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -626,9 +626,9 @@ public void accept( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - classVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + classVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -650,9 +650,9 @@ public void accept( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ true), + /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -674,9 +674,9 @@ public void accept( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ false), + /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -968,9 +968,9 @@ private int readRecordComponent( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -986,9 +986,9 @@ private int readRecordComponent( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1010,9 +1010,9 @@ private int readRecordComponent( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ true), + /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1034,9 +1034,9 @@ private int readRecordComponent( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ false), + /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1152,9 +1152,9 @@ private int readField( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - fieldVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + fieldVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1170,9 +1170,9 @@ private int readField( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - fieldVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + fieldVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1194,9 +1194,9 @@ private int readField( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ true), + /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1218,9 +1218,9 @@ private int readField( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ false), + /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1413,9 +1413,9 @@ private int readMethod( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - methodVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + methodVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1431,9 +1431,9 @@ private int readMethod( // Parse num_element_value_pairs and element_value_pairs and visit these values. currentAnnotationOffset = readElementValues( - methodVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + methodVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1455,9 +1455,9 @@ private int readMethod( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ true), + /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1479,9 +1479,9 @@ private int readMethod( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ false), + /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -1489,16 +1489,13 @@ private int readMethod( // Visit the RuntimeVisibleParameterAnnotations attribute. if (runtimeVisibleParameterAnnotationsOffset != 0) { readParameterAnnotations( - methodVisitor, context, runtimeVisibleParameterAnnotationsOffset, /* visible = */ true); + methodVisitor, context, runtimeVisibleParameterAnnotationsOffset, /* visible= */ true); } // Visit the RuntimeInvisibleParameterAnnotations attribute. if (runtimeInvisibleParameterAnnotationsOffset != 0) { readParameterAnnotations( - methodVisitor, - context, - runtimeInvisibleParameterAnnotationsOffset, - /* visible = */ false); + methodVisitor, context, runtimeInvisibleParameterAnnotationsOffset, /* visible= */ false); } // Visit the non standard attributes. @@ -1927,7 +1924,7 @@ private void readCode( } } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { visibleTypeAnnotationOffsets = - readTypeAnnotations(methodVisitor, context, currentOffset, /* visible = */ true); + readTypeAnnotations(methodVisitor, context, currentOffset, /* visible= */ true); // Here we do not extract the labels corresponding to the attribute content. This would // require a full parsing of the attribute, which would need to be repeated when parsing // the bytecode instructions (see below). Instead, the content of the attribute is read one @@ -1936,7 +1933,7 @@ private void readCode( // time. This assumes that type annotations are ordered by increasing bytecode offset. } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { invisibleTypeAnnotationOffsets = - readTypeAnnotations(methodVisitor, context, currentOffset, /* visible = */ false); + readTypeAnnotations(methodVisitor, context, currentOffset, /* visible= */ false); // Same comment as above for the RuntimeVisibleTypeAnnotations attribute. } else if (Constants.STACK_MAP_TABLE.equals(attributeName)) { if ((context.parsingOptions & SKIP_FRAMES) == 0) { @@ -2052,6 +2049,7 @@ private void readCode( currentOffset = bytecodeStartOffset; while (currentOffset < bytecodeEndOffset) { final int currentBytecodeOffset = currentOffset - bytecodeStartOffset; + readBytecodeInstructionOffset(currentBytecodeOffset); // Visit the label and the line number(s) for this bytecode offset, if any. Label currentLabel = labels[currentBytecodeOffset]; @@ -2517,9 +2515,9 @@ private void readCode( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ true), + /* visible= */ true), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } currentVisibleTypeAnnotationBytecodeOffset = @@ -2545,9 +2543,9 @@ private void readCode( context.currentTypeAnnotationTarget, context.currentTypeAnnotationTargetPath, annotationDescriptor, - /* visible = */ false), + /* visible= */ false), currentAnnotationOffset, - /* named = */ true, + /* named= */ true, charBuffer); } currentInvisibleTypeAnnotationBytecodeOffset = @@ -2618,9 +2616,9 @@ private void readCode( context.currentLocalVariableAnnotationRangeEnds, context.currentLocalVariableAnnotationRangeIndices, annotationDescriptor, - /* visible = */ true), + /* visible= */ true), currentOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -2646,9 +2644,9 @@ private void readCode( context.currentLocalVariableAnnotationRangeEnds, context.currentLocalVariableAnnotationRangeIndices, annotationDescriptor, - /* visible = */ false), + /* visible= */ false), currentOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -2667,6 +2665,20 @@ private void readCode( methodVisitor.visitMaxs(maxStack, maxLocals); } + /** + * Handles the bytecode offset of the next instruction to be visited in {@link + * #accept(ClassVisitor,int)}. This method is called just before the instruction and before its + * associated label and stack map frame, if any. The default implementation of this method does + * nothing. Subclasses can override this method to store the argument in a mutable field, for + * instance, so that {@link MethodVisitor} instances can get the bytecode offset of each visited + * instruction (if so, the usual concurrency issues related to mutable data should be addressed). + * + * @param bytecodeOffset the bytecode offset of the next instruction to be visited. + */ + protected void readBytecodeInstructionOffset(final int bytecodeOffset) { + // Do nothing by default. + } + /** * Returns the label corresponding to the given bytecode offset. The default implementation of * this method creates a label for the given offset if it has not been already created. @@ -2812,7 +2824,7 @@ private int[] readTypeAnnotations( methodVisitor.visitTryCatchAnnotation( targetType & 0xFFFFFF00, path, annotationDescriptor, visible), currentOffset, - /* named = */ true, + /* named= */ true, charBuffer); } else { // We don't want to visit the other target_type annotations, so we just skip them (which @@ -2823,7 +2835,7 @@ private int[] readTypeAnnotations( // with a null AnnotationVisitor). currentOffset = readElementValues( - /* annotationVisitor = */ null, currentOffset, /* named = */ true, charBuffer); + /* annotationVisitor= */ null, currentOffset, /* named= */ true, charBuffer); } } return typeAnnotationsOffsets; @@ -2963,7 +2975,7 @@ private void readParameterAnnotations( readElementValues( methodVisitor.visitParameterAnnotation(i, annotationDescriptor, visible), currentOffset, - /* named = */ true, + /* named= */ true, charBuffer); } } @@ -3033,9 +3045,9 @@ private int readElementValue( case 'e': // enum_const_value return currentOffset + 5; case '@': // annotation_value - return readElementValues(null, currentOffset + 3, /* named = */ true, charBuffer); + return readElementValues(null, currentOffset + 3, /* named= */ true, charBuffer); case '[': // array_value - return readElementValues(null, currentOffset + 1, /* named = */ false, charBuffer); + return readElementValues(null, currentOffset + 1, /* named= */ false, charBuffer); default: return currentOffset + 3; } @@ -3103,7 +3115,7 @@ private int readElementValue( return readElementValues( annotationVisitor.visitArray(elementName), currentOffset - 2, - /* named = */ false, + /* named= */ false, charBuffer); } switch (classFileBuffer[currentOffset] & 0xFF) { @@ -3180,7 +3192,7 @@ private int readElementValue( readElementValues( annotationVisitor.visitArray(elementName), currentOffset - 2, - /* named = */ false, + /* named= */ false, charBuffer); break; } diff --git a/spring-core/src/main/java/org/springframework/asm/ClassWriter.java b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java index 030d86566f61..676cd0584b21 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassWriter.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java @@ -217,6 +217,7 @@ public class ClassWriter extends ClassVisitor { /** * Indicates what must be automatically computed in {@link MethodWriter}. Must be one of {@link * MethodWriter#COMPUTE_NOTHING}, {@link MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL}, {@link + * MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link * MethodWriter#COMPUTE_INSERTED_FRAMES}, or {@link MethodWriter#COMPUTE_ALL_FRAMES}. */ private int compute; @@ -773,7 +774,7 @@ private byte[] replaceAsmInstructions(final byte[] classFile, final boolean hasF lastRecordComponent = null; firstAttribute = null; compute = hasFrames ? MethodWriter.COMPUTE_INSERTED_FRAMES : MethodWriter.COMPUTE_NOTHING; - new ClassReader(classFile, 0, /* checkClassVersion = */ false) + new ClassReader(classFile, 0, /* checkClassVersion= */ false) .accept( this, attributes, diff --git a/spring-core/src/main/java/org/springframework/asm/Frame.java b/spring-core/src/main/java/org/springframework/asm/Frame.java index 76317cc91c31..d70e18096ac2 100644 --- a/spring-core/src/main/java/org/springframework/asm/Frame.java +++ b/spring-core/src/main/java/org/springframework/asm/Frame.java @@ -64,8 +64,8 @@ * right shift of {@link #DIM_SHIFT}. *
  • the KIND field, stored in 4 bits, indicates the kind of VALUE used. These 4 bits can be * retrieved with {@link #KIND_MASK} and, without any shift, must be equal to {@link - * #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link #LOCAL_KIND} - * or {@link #STACK_KIND}. + * #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link + * #FORWARD_UNINITIALIZED_KIND},{@link #LOCAL_KIND} or {@link #STACK_KIND}. *
  • the FLAGS field, stored in 2 bits, contains up to 2 boolean flags. Currently only one flag * is defined, namely {@link #TOP_IF_LONG_OR_DOUBLE_FLAG}. *
  • the VALUE field, stored in the remaining 20 bits, contains either @@ -78,7 +78,10 @@ *
  • the index of a {@link Symbol#TYPE_TAG} {@link Symbol} in the type table of a {@link * SymbolTable}, if KIND is equal to {@link #REFERENCE_KIND}. *
  • the index of an {@link Symbol#UNINITIALIZED_TYPE_TAG} {@link Symbol} in the type - * table of a SymbolTable, if KIND is equal to {@link #UNINITIALIZED_KIND}. + * table of a {@link SymbolTable}, if KIND is equal to {@link #UNINITIALIZED_KIND}. + *
  • the index of a {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG} {@link Symbol} in the + * type table of a {@link SymbolTable}, if KIND is equal to {@link + * #FORWARD_UNINITIALIZED_KIND}. *
  • the index of a local variable in the input stack frame, if KIND is equal to {@link * #LOCAL_KIND}. *
  • a position relatively to the top of the stack of the input stack frame, if KIND is @@ -88,10 +91,10 @@ * *

    Output frames can contain abstract types of any kind and with a positive or negative array * dimension (and even unassigned types, represented by 0 - which does not correspond to any valid - * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND or - * UNINITIALIZED_KIND abstract types of positive or {@literal null} array dimension. In all cases - * the type table contains only internal type names (array type descriptors are forbidden - array - * dimensions must be represented through the DIM field). + * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND, + * UNINITIALIZED_KIND or FORWARD_UNINITIALIZED_KIND abstract types of positive or {@literal null} + * array dimension. In all cases the type table contains only internal type names (array type + * descriptors are forbidden - array dimensions must be represented through the DIM field). * *

    The LONG and DOUBLE types are always represented by using two slots (LONG + TOP or DOUBLE + * TOP), for local variables as well as in the operand stack. This is necessary to be able to @@ -159,8 +162,9 @@ class Frame { private static final int CONSTANT_KIND = 1 << KIND_SHIFT; private static final int REFERENCE_KIND = 2 << KIND_SHIFT; private static final int UNINITIALIZED_KIND = 3 << KIND_SHIFT; - private static final int LOCAL_KIND = 4 << KIND_SHIFT; - private static final int STACK_KIND = 5 << KIND_SHIFT; + private static final int FORWARD_UNINITIALIZED_KIND = 4 << KIND_SHIFT; + private static final int LOCAL_KIND = 5 << KIND_SHIFT; + private static final int STACK_KIND = 6 << KIND_SHIFT; // Possible flags for the FLAGS field of an abstract type. @@ -220,13 +224,13 @@ class Frame { /** * The abstract types that are initialized in the basic block. A constructor invocation on an - * UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace every occurrence of this - * type in the local variables and in the operand stack. This cannot be done during the first step - * of the algorithm since, during this step, the local variables and the operand stack types are - * still abstract. It is therefore necessary to store the abstract types of the constructors which - * are invoked in the basic block, in order to do this replacement during the second step of the - * algorithm, where the frames are fully computed. Note that this array can contain abstract types - * that are relative to the input locals or to the input stack. + * UNINITIALIZED, FORWARD_UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace every + * occurrence of this type in the local variables and in the operand stack. This cannot be + * done during the first step of the algorithm since, during this step, the local variables and + * the operand stack types are still abstract. It is therefore necessary to store the abstract + * types of the constructors which are invoked in the basic block, in order to do this replacement + * during the second step of the algorithm, where the frames are fully computed. Note that this + * array can contain abstract types that are relative to the input locals or to the input stack. */ private int[] initializations; @@ -284,8 +288,12 @@ static int getAbstractTypeFromApiFormat(final SymbolTable symbolTable, final Obj String descriptor = Type.getObjectType((String) type).getDescriptor(); return getAbstractTypeFromDescriptor(symbolTable, descriptor, 0); } else { - return UNINITIALIZED_KIND - | symbolTable.addUninitializedType("", ((Label) type).bytecodeOffset); + Label label = (Label) type; + if ((label.flags & Label.FLAG_RESOLVED) != 0) { + return UNINITIALIZED_KIND | symbolTable.addUninitializedType("", label.bytecodeOffset); + } else { + return FORWARD_UNINITIALIZED_KIND | symbolTable.addForwardUninitializedType("", label); + } } } @@ -637,12 +645,14 @@ private void addInitializedType(final int abstractType) { * @param symbolTable the type table to use to lookup and store type {@link Symbol}. * @param abstractType an abstract type. * @return the REFERENCE_KIND abstract type corresponding to abstractType if it is - * UNINITIALIZED_THIS or an UNINITIALIZED_KIND abstract type for one of the types on which a - * constructor is invoked in the basic block. Otherwise returns abstractType. + * UNINITIALIZED_THIS or an UNINITIALIZED_KIND or FORWARD_UNINITIALIZED_KIND abstract type for + * one of the types on which a constructor is invoked in the basic block. Otherwise returns + * abstractType. */ private int getInitializedType(final SymbolTable symbolTable, final int abstractType) { if (abstractType == UNINITIALIZED_THIS - || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND) { + || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND + || (abstractType & (DIM_MASK | KIND_MASK)) == FORWARD_UNINITIALIZED_KIND) { for (int i = 0; i < initializationCount; ++i) { int initializedType = initializations[i]; int dim = initializedType & DIM_MASK; @@ -1253,11 +1263,12 @@ final boolean merge( * * @param symbolTable the type table to use to lookup and store type {@link Symbol}. * @param sourceType the abstract type with which the abstract type array element must be merged. - * This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND} or {@link - * #UNINITIALIZED_KIND} kind, with positive or {@literal null} array dimensions. + * This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link + * #UNINITIALIZED_KIND} or {@link #FORWARD_UNINITIALIZED_KIND} kind, with positive or + * {@literal null} array dimensions. * @param dstTypes an array of abstract types. These types should be of {@link #CONSTANT_KIND}, - * {@link #REFERENCE_KIND} or {@link #UNINITIALIZED_KIND} kind, with positive or {@literal - * null} array dimensions. + * {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND} or {@link #FORWARD_UNINITIALIZED_KIND} + * kind, with positive or {@literal null} array dimensions. * @param dstIndex the index of the type that must be merged in dstTypes. * @return {@literal true} if the type array has been modified by this operation. */ @@ -1400,7 +1411,8 @@ final void accept(final MethodWriter methodWriter) { * * @param symbolTable the type table to use to lookup and store type {@link Symbol}. * @param abstractType an abstract type, restricted to {@link Frame#CONSTANT_KIND}, {@link - * Frame#REFERENCE_KIND} or {@link Frame#UNINITIALIZED_KIND} types. + * Frame#REFERENCE_KIND}, {@link Frame#UNINITIALIZED_KIND} or {@link + * Frame#FORWARD_UNINITIALIZED_KIND} types. * @param output where the abstract type must be put. * @see JVMS * 4.7.4 @@ -1422,6 +1434,10 @@ static void putAbstractType( case UNINITIALIZED_KIND: output.putByte(ITEM_UNINITIALIZED).putShort((int) symbolTable.getType(typeValue).data); break; + case FORWARD_UNINITIALIZED_KIND: + output.putByte(ITEM_UNINITIALIZED); + symbolTable.getForwardUninitializedLabel(typeValue).put(output); + break; default: throw new AssertionError(); } diff --git a/spring-core/src/main/java/org/springframework/asm/Label.java b/spring-core/src/main/java/org/springframework/asm/Label.java index e9e2f9e0d2e6..da840f103173 100644 --- a/spring-core/src/main/java/org/springframework/asm/Label.java +++ b/spring-core/src/main/java/org/springframework/asm/Label.java @@ -81,6 +81,9 @@ public class Label { /** A flag indicating that the basic block corresponding to a label is the end of a subroutine. */ static final int FLAG_SUBROUTINE_END = 64; + /** A flag indicating that this label has at least one associated line number. */ + static final int FLAG_LINE_NUMBER = 128; + /** * The number of elements to add to the {@link #otherLineNumbers} array when it needs to be * resized to store a new source line number. @@ -113,6 +116,13 @@ public class Label { */ static final int FORWARD_REFERENCE_TYPE_WIDE = 0x20000000; + /** + * The type of forward references stored in two bytes in the stack map table. This is the + * case of the labels of {@link Frame#ITEM_UNINITIALIZED} stack map frame elements, when the NEW + * instruction is after the <init> constructor call (in bytecode offset order). + */ + static final int FORWARD_REFERENCE_TYPE_STACK_MAP = 0x30000000; + /** * The bit mask to extract the 'handle' of a forward reference to this label. The extracted handle * is the bytecode offset where the forward reference value is stored (using either 2 or 4 bytes, @@ -145,9 +155,9 @@ public class Label { short flags; /** - * The source line number corresponding to this label, or 0. If there are several source line - * numbers corresponding to this label, the first one is stored in this field, and the remaining - * ones are stored in {@link #otherLineNumbers}. + * The source line number corresponding to this label, if {@link #FLAG_LINE_NUMBER} is set. If + * there are several source line numbers corresponding to this label, the first one is stored in + * this field, and the remaining ones are stored in {@link #otherLineNumbers}. */ private short lineNumber; @@ -332,7 +342,8 @@ final Label getCanonicalInstance() { * @param lineNumber a source line number (which should be strictly positive). */ final void addLineNumber(final int lineNumber) { - if (this.lineNumber == 0) { + if ((flags & FLAG_LINE_NUMBER) == 0) { + flags |= FLAG_LINE_NUMBER; this.lineNumber = (short) lineNumber; } else { if (otherLineNumbers == null) { @@ -356,7 +367,7 @@ final void addLineNumber(final int lineNumber) { */ final void accept(final MethodVisitor methodVisitor, final boolean visitLineNumbers) { methodVisitor.visitLabel(this); - if (visitLineNumbers && lineNumber != 0) { + if (visitLineNumbers && (flags & FLAG_LINE_NUMBER) != 0) { methodVisitor.visitLineNumber(lineNumber & 0xFFFF, this); if (otherLineNumbers != null) { for (int i = 1; i <= otherLineNumbers[0]; ++i) { @@ -400,6 +411,20 @@ final void put( } } + /** + * Puts a reference to this label in the stack map table of a method. If the bytecode + * offset of the label is known, it is written directly. Otherwise, a null relative offset is + * written and a new forward reference is declared for this label. + * + * @param stackMapTableEntries the stack map table where the label offset must be added. + */ + final void put(final ByteVector stackMapTableEntries) { + if ((flags & FLAG_RESOLVED) == 0) { + addForwardReference(0, FORWARD_REFERENCE_TYPE_STACK_MAP, stackMapTableEntries.length); + } + stackMapTableEntries.putShort(bytecodeOffset); + } + /** * Adds a forward reference to this label. This method must be called only for a true forward * reference, i.e. only if this label is not resolved yet. For backward references, the relative @@ -432,9 +457,12 @@ private void addForwardReference( * Sets the bytecode offset of this label to the given value and resolves the forward references * to this label, if any. This method must be called when this label is added to the bytecode of * the method, i.e. when its bytecode offset becomes known. This method fills in the blanks that - * where left in the bytecode by each forward reference previously added to this label. + * where left in the bytecode (and optionally in the stack map table) by each forward reference + * previously added to this label. * * @param code the bytecode of the method. + * @param stackMapTableEntries the 'entries' array of the StackMapTable code attribute of the + * method. Maybe {@literal null}. * @param bytecodeOffset the bytecode offset of this label. * @return {@literal true} if a blank that was left for this label was too small to store the * offset. In such a case the corresponding jump instruction is replaced with an equivalent @@ -442,7 +470,8 @@ private void addForwardReference( * instructions are later replaced with standard bytecode instructions with wider offsets (4 * bytes instead of 2), in ClassReader. */ - final boolean resolve(final byte[] code, final int bytecodeOffset) { + final boolean resolve( + final byte[] code, final ByteVector stackMapTableEntries, final int bytecodeOffset) { this.flags |= FLAG_RESOLVED; this.bytecodeOffset = bytecodeOffset; if (forwardReferences == null) { @@ -472,11 +501,14 @@ final boolean resolve(final byte[] code, final int bytecodeOffset) { } code[handle++] = (byte) (relativeOffset >>> 8); code[handle] = (byte) relativeOffset; - } else { + } else if ((reference & FORWARD_REFERENCE_TYPE_MASK) == FORWARD_REFERENCE_TYPE_WIDE) { code[handle++] = (byte) (relativeOffset >>> 24); code[handle++] = (byte) (relativeOffset >>> 16); code[handle++] = (byte) (relativeOffset >>> 8); code[handle] = (byte) relativeOffset; + } else { + stackMapTableEntries.data[handle++] = (byte) (bytecodeOffset >>> 8); + stackMapTableEntries.data[handle] = (byte) bytecodeOffset; } } return hasAsmInstructions; diff --git a/spring-core/src/main/java/org/springframework/asm/MethodWriter.java b/spring-core/src/main/java/org/springframework/asm/MethodWriter.java index 58fa599721f2..543e8a3c6a17 100644 --- a/spring-core/src/main/java/org/springframework/asm/MethodWriter.java +++ b/spring-core/src/main/java/org/springframework/asm/MethodWriter.java @@ -534,8 +534,9 @@ final class MethodWriter extends MethodVisitor { * the number of stack elements. The local variables start at index 3 and are followed by the * operand stack elements. In summary frame[0] = offset, frame[1] = numLocal, frame[2] = numStack. * Local variables and operand stack entries contain abstract types, as defined in {@link Frame}, - * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND} or {@link - * Frame#UNINITIALIZED_KIND} abstract types. Long and double types use only one array entry. + * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND}, {@link + * Frame#UNINITIALIZED_KIND} or {@link Frame#FORWARD_UNINITIALIZED_KIND} abstract types. Long and + * double types use only one array entry. */ private int[] currentFrame; @@ -650,7 +651,7 @@ public void visitParameter(final String name, final int access) { @Override public AnnotationVisitor visitAnnotationDefault() { defaultValue = new ByteVector(); - return new AnnotationWriter(symbolTable, /* useNamedValues = */ false, defaultValue, null); + return new AnnotationWriter(symbolTable, /* useNamedValues= */ false, defaultValue, null); } @Override @@ -693,7 +694,7 @@ public AnnotationVisitor visitParameterAnnotation( if (visible) { if (lastRuntimeVisibleParameterAnnotations == null) { lastRuntimeVisibleParameterAnnotations = - new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + new AnnotationWriter[Type.getArgumentCount(descriptor)]; } return lastRuntimeVisibleParameterAnnotations[parameter] = AnnotationWriter.create( @@ -701,7 +702,7 @@ public AnnotationVisitor visitParameterAnnotation( } else { if (lastRuntimeInvisibleParameterAnnotations == null) { lastRuntimeInvisibleParameterAnnotations = - new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + new AnnotationWriter[Type.getArgumentCount(descriptor)]; } return lastRuntimeInvisibleParameterAnnotations[parameter] = AnnotationWriter.create( @@ -1199,7 +1200,7 @@ public void visitJumpInsn(final int opcode, final Label label) { @Override public void visitLabel(final Label label) { // Resolve the forward references to this label, if any. - hasAsmInstructions |= label.resolve(code.data, code.length); + hasAsmInstructions |= label.resolve(code.data, stackMapTableEntries, code.length); // visitLabel starts a new basic block (except for debug only labels), so we need to update the // previous and current block references and list of successors. if ((label.flags & Label.FLAG_DEBUG_ONLY) != 0) { @@ -1518,14 +1519,14 @@ public AnnotationVisitor visitLocalVariableAnnotation( return lastCodeRuntimeVisibleTypeAnnotation = new AnnotationWriter( symbolTable, - /* useNamedValues = */ true, + /* useNamedValues= */ true, typeAnnotation, lastCodeRuntimeVisibleTypeAnnotation); } else { return lastCodeRuntimeInvisibleTypeAnnotation = new AnnotationWriter( symbolTable, - /* useNamedValues = */ true, + /* useNamedValues= */ true, typeAnnotation, lastCodeRuntimeInvisibleTypeAnnotation); } @@ -1795,7 +1796,7 @@ private void endCurrentBasicBlockWithNoSuccessor() { if (compute == COMPUTE_ALL_FRAMES) { Label nextBasicBlock = new Label(); nextBasicBlock.frame = new Frame(nextBasicBlock); - nextBasicBlock.resolve(code.data, code.length); + nextBasicBlock.resolve(code.data, stackMapTableEntries, code.length); lastBasicBlock.nextBasicBlock = nextBasicBlock; lastBasicBlock = nextBasicBlock; currentBasicBlock = null; @@ -1979,9 +1980,8 @@ private void putFrameType(final Object type) { .putByte(Frame.ITEM_OBJECT) .putShort(symbolTable.addConstantClass((String) type).index); } else { - stackMapTableEntries - .putByte(Frame.ITEM_UNINITIALIZED) - .putShort(((Label) type).bytecodeOffset); + stackMapTableEntries.putByte(Frame.ITEM_UNINITIALIZED); + ((Label) type).put(stackMapTableEntries); } } diff --git a/spring-core/src/main/java/org/springframework/asm/Opcodes.java b/spring-core/src/main/java/org/springframework/asm/Opcodes.java index 035aa4548477..a047b88c096f 100644 --- a/spring-core/src/main/java/org/springframework/asm/Opcodes.java +++ b/spring-core/src/main/java/org/springframework/asm/Opcodes.java @@ -286,6 +286,8 @@ public interface Opcodes { int V19 = 0 << 16 | 63; int V20 = 0 << 16 | 64; int V21 = 0 << 16 | 65; + int V22 = 0 << 16 | 66; + int V23 = 0 << 16 | 67; /** * Version flag indicating that the class is using 'preview' features. diff --git a/spring-core/src/main/java/org/springframework/asm/Symbol.java b/spring-core/src/main/java/org/springframework/asm/Symbol.java index 1088bbff0b5e..ce67127aa92c 100644 --- a/spring-core/src/main/java/org/springframework/asm/Symbol.java +++ b/spring-core/src/main/java/org/springframework/asm/Symbol.java @@ -103,12 +103,25 @@ abstract class Symbol { static final int TYPE_TAG = 128; /** - * The tag value of an {@link Frame#ITEM_UNINITIALIZED} type entry in the type table of a class. + * The tag value of an uninitialized type entry in the type table of a class. This type is used + * for the normal case where the NEW instruction is before the <init> constructor call (in + * bytecode offset order), i.e. when the label of the NEW instruction is resolved when the + * constructor call is visited. If the NEW instruction is after the constructor call, use the + * {@link #FORWARD_UNINITIALIZED_TYPE_TAG} tag value instead. */ static final int UNINITIALIZED_TYPE_TAG = 129; + /** + * The tag value of an uninitialized type entry in the type table of a class. This type is used + * for the unusual case where the NEW instruction is after the <init> constructor call (in + * bytecode offset order), i.e. when the label of the NEW instruction is not resolved when the + * constructor call is visited. If the NEW instruction is before the constructor call, use the + * {@link #UNINITIALIZED_TYPE_TAG} tag value instead. + */ + static final int FORWARD_UNINITIALIZED_TYPE_TAG = 130; + /** The tag value of a merged type entry in the (ASM specific) type table of a class. */ - static final int MERGED_TYPE_TAG = 130; + static final int MERGED_TYPE_TAG = 131; // Instance fields. @@ -151,8 +164,8 @@ abstract class Symbol { * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, *

  • an arbitrary string for {@link #CONSTANT_UTF8_TAG} and {@link #CONSTANT_STRING_TAG} * symbols, - *
  • an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG} and {@link - * #UNINITIALIZED_TYPE_TAG} symbols, + *
  • an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG}, {@link + * #UNINITIALIZED_TYPE_TAG} and {@link #FORWARD_UNINITIALIZED_TYPE_TAG} symbols, *
  • {@literal null} for the other types of symbol. * */ @@ -172,6 +185,9 @@ abstract class Symbol { * {@link #CONSTANT_DYNAMIC_TAG} or {@link #BOOTSTRAP_METHOD_TAG} symbols, *
  • the bytecode offset of the NEW instruction that created an {@link * Frame#ITEM_UNINITIALIZED} type for {@link #UNINITIALIZED_TYPE_TAG} symbols, + *
  • the index of the {@link Label} (in the {@link SymbolTable#labelTable} table) of the NEW + * instruction that created an {@link Frame#ITEM_UNINITIALIZED} type for {@link + * #FORWARD_UNINITIALIZED_TYPE_TAG} symbols, *
  • the indices (in the class' type table) of two {@link #TYPE_TAG} source types for {@link * #MERGED_TYPE_TAG} symbols, *
  • 0 for the other types of symbol. diff --git a/spring-core/src/main/java/org/springframework/asm/SymbolTable.java b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java index e4c4a8461e71..7e0e7f82fe71 100644 --- a/spring-core/src/main/java/org/springframework/asm/SymbolTable.java +++ b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java @@ -108,11 +108,35 @@ final class SymbolTable { * An ASM specific type table used to temporarily store internal names that will not necessarily * be stored in the constant pool. This type table is used by the control flow and data flow * analysis algorithm used to compute stack map frames from scratch. This array stores {@link - * Symbol#TYPE_TAG} and {@link Symbol#UNINITIALIZED_TYPE_TAG}) Symbol. The type symbol at index - * {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa). + * Symbol#TYPE_TAG}, {@link Symbol#UNINITIALIZED_TYPE_TAG},{@link + * Symbol#FORWARD_UNINITIALIZED_TYPE_TAG} and {@link Symbol#MERGED_TYPE_TAG} entries. The type + * symbol at index {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa). */ private Entry[] typeTable; + /** + * The actual number of {@link LabelEntry} in {@link #labelTable}. These elements are stored from + * index 0 to labelCount (excluded). The other array entries are empty. These label entries are + * also stored in the {@link #labelEntries} hash set. + */ + private int labelCount; + + /** + * The labels corresponding to the "forward uninitialized" types in the ASM specific {@link + * typeTable} (see {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}). The label entry at index {@code + * i} has its {@link LabelEntry#index} equal to {@code i} (and vice versa). + */ + private LabelEntry[] labelTable; + + /** + * A hash set of all the {@link LabelEntry} elements in the {@link #labelTable}. Each {@link + * LabelEntry} instance is stored at the array index given by its hash code modulo the array size. + * If several entries must be stored at the same array index, they are linked together via their + * {@link LabelEntry#next} field. The {@link #getOrAddLabelEntry(Label)} method ensures that this + * table does not contain duplicated entries. + */ + private LabelEntry[] labelEntries; + /** * Constructs a new, empty SymbolTable for the given ClassWriter. * @@ -1129,6 +1153,18 @@ Symbol getType(final int typeIndex) { return typeTable[typeIndex]; } + /** + * Returns the label corresponding to the "forward uninitialized" type table element whose index + * is given. + * + * @param typeIndex the type table index of a "forward uninitialized" type table element. + * @return the label corresponding of the NEW instruction which created this "forward + * uninitialized" type. + */ + Label getForwardUninitializedLabel(final int typeIndex) { + return labelTable[(int) typeTable[typeIndex].data].label; + } + /** * Adds a type in the type table of this symbol table. Does nothing if the type table already * contains a similar type. @@ -1149,13 +1185,13 @@ int addType(final String value) { } /** - * Adds an {@link Frame#ITEM_UNINITIALIZED} type in the type table of this symbol table. Does - * nothing if the type table already contains a similar type. + * Adds an uninitialized type in the type table of this symbol table. Does nothing if the type + * table already contains a similar type. * * @param value an internal class name. - * @param bytecodeOffset the bytecode offset of the NEW instruction that created this {@link - * Frame#ITEM_UNINITIALIZED} type value. - * @return the index of a new or already existing type Symbol with the given value. + * @param bytecodeOffset the bytecode offset of the NEW instruction that created this + * uninitialized type value. + * @return the index of a new or already existing type #@link Symbol} with the given value. */ int addUninitializedType(final String value, final int bytecodeOffset) { int hashCode = hash(Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset); @@ -1173,6 +1209,32 @@ int addUninitializedType(final String value, final int bytecodeOffset) { new Entry(typeCount, Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset, hashCode)); } + /** + * Adds a "forward uninitialized" type in the type table of this symbol table. Does nothing if the + * type table already contains a similar type. + * + * @param value an internal class name. + * @param label the label of the NEW instruction that created this uninitialized type value. If + * the label is resolved, use the {@link #addUninitializedType} method instead. + * @return the index of a new or already existing type {@link Symbol} with the given value. + */ + int addForwardUninitializedType(final String value, final Label label) { + int labelIndex = getOrAddLabelEntry(label).index; + int hashCode = hash(Symbol.FORWARD_UNINITIALIZED_TYPE_TAG, value, labelIndex); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.FORWARD_UNINITIALIZED_TYPE_TAG + && entry.hashCode == hashCode + && entry.data == labelIndex + && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + return addTypeInternal( + new Entry(typeCount, Symbol.FORWARD_UNINITIALIZED_TYPE_TAG, value, labelIndex, hashCode)); + } + /** * Adds a merged type in the type table of this symbol table. Does nothing if the type table * already contains a similar type. @@ -1225,6 +1287,59 @@ private int addTypeInternal(final Entry entry) { return put(entry).index; } + /** + * Returns the {@link LabelEntry} corresponding to the given label. Creates a new one if there is + * no such entry. + * + * @param label the {@link Label} of a NEW instruction which created an uninitialized type, in the + * case where this NEW instruction is after the <init> constructor call (in bytecode + * offset order). See {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}. + * @return the {@link LabelEntry} corresponding to {@code label}. + */ + private LabelEntry getOrAddLabelEntry(final Label label) { + if (labelEntries == null) { + labelEntries = new LabelEntry[16]; + labelTable = new LabelEntry[16]; + } + int hashCode = System.identityHashCode(label); + LabelEntry labelEntry = labelEntries[hashCode % labelEntries.length]; + while (labelEntry != null && labelEntry.label != label) { + labelEntry = labelEntry.next; + } + if (labelEntry != null) { + return labelEntry; + } + + if (labelCount > (labelEntries.length * 3) / 4) { + int currentCapacity = labelEntries.length; + int newCapacity = currentCapacity * 2 + 1; + LabelEntry[] newLabelEntries = new LabelEntry[newCapacity]; + for (int i = currentCapacity - 1; i >= 0; --i) { + LabelEntry currentEntry = labelEntries[i]; + while (currentEntry != null) { + int newCurrentEntryIndex = System.identityHashCode(currentEntry.label) % newCapacity; + LabelEntry nextEntry = currentEntry.next; + currentEntry.next = newLabelEntries[newCurrentEntryIndex]; + newLabelEntries[newCurrentEntryIndex] = currentEntry; + currentEntry = nextEntry; + } + } + labelEntries = newLabelEntries; + } + if (labelCount == labelTable.length) { + LabelEntry[] newLabelTable = new LabelEntry[2 * labelTable.length]; + System.arraycopy(labelTable, 0, newLabelTable, 0, labelTable.length); + labelTable = newLabelTable; + } + + labelEntry = new LabelEntry(labelCount, label); + int index = hashCode % labelEntries.length; + labelEntry.next = labelEntries[index]; + labelEntries[index] = labelEntry; + labelTable[labelCount++] = labelEntry; + return labelEntry; + } + // ----------------------------------------------------------------------------------------------- // Static helper methods to compute hash codes. // ----------------------------------------------------------------------------------------------- @@ -1275,7 +1390,7 @@ private static int hash( * * @author Eric Bruneton */ - private static class Entry extends Symbol { + private static final class Entry extends Symbol { /** The hash code of this entry. */ final int hashCode; @@ -1299,24 +1414,50 @@ private static class Entry extends Symbol { } Entry(final int index, final int tag, final String value, final int hashCode) { - super(index, tag, /* owner = */ null, /* name = */ null, value, /* data = */ 0); + super(index, tag, /* owner= */ null, /* name= */ null, value, /* data= */ 0); this.hashCode = hashCode; } Entry(final int index, final int tag, final String value, final long data, final int hashCode) { - super(index, tag, /* owner = */ null, /* name = */ null, value, data); + super(index, tag, /* owner= */ null, /* name= */ null, value, data); this.hashCode = hashCode; } Entry( final int index, final int tag, final String name, final String value, final int hashCode) { - super(index, tag, /* owner = */ null, name, value, /* data = */ 0); + super(index, tag, /* owner= */ null, name, value, /* data= */ 0); this.hashCode = hashCode; } Entry(final int index, final int tag, final long data, final int hashCode) { - super(index, tag, /* owner = */ null, /* name = */ null, /* value = */ null, data); + super(index, tag, /* owner= */ null, /* name= */ null, /* value= */ null, data); this.hashCode = hashCode; } } + + /** + * A label corresponding to a "forward uninitialized" type in the ASM specific {@link + * SymbolTable#typeTable} (see {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}). + * + * @author Eric Bruneton + */ + private static final class LabelEntry { + + /** The index of this label entry in the {@link SymbolTable#labelTable} array. */ + final int index; + + /** The value of this label entry. */ + final Label label; + + /** + * Another entry (and so on recursively) having the same hash code (modulo the size of {@link + * SymbolTable#labelEntries}}) as this one. + */ + LabelEntry next; + + LabelEntry(final int index, final Label label) { + this.index = index; + this.label = label; + } + } } diff --git a/spring-core/src/main/java/org/springframework/asm/Type.java b/spring-core/src/main/java/org/springframework/asm/Type.java index 36d69ab8b08c..7e35a139c08d 100644 --- a/spring-core/src/main/java/org/springframework/asm/Type.java +++ b/spring-core/src/main/java/org/springframework/asm/Type.java @@ -295,26 +295,12 @@ public Type[] getArgumentTypes() { */ public static Type[] getArgumentTypes(final String methodDescriptor) { // First step: compute the number of argument types in methodDescriptor. - int numArgumentTypes = 0; - // Skip the first character, which is always a '('. - int currentOffset = 1; - // Parse the argument types, one at a each loop iteration. - while (methodDescriptor.charAt(currentOffset) != ')') { - while (methodDescriptor.charAt(currentOffset) == '[') { - currentOffset++; - } - if (methodDescriptor.charAt(currentOffset++) == 'L') { - // Skip the argument descriptor content. - int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); - currentOffset = Math.max(currentOffset, semiColumnOffset + 1); - } - ++numArgumentTypes; - } + int numArgumentTypes = getArgumentCount(methodDescriptor); // Second step: create a Type instance for each argument type. Type[] argumentTypes = new Type[numArgumentTypes]; // Skip the first character, which is always a '('. - currentOffset = 1; + int currentOffset = 1; // Parse and create the argument types, one at each loop iteration. int currentArgumentTypeIndex = 0; while (methodDescriptor.charAt(currentOffset) != ')') { @@ -614,7 +600,7 @@ private static void appendDescriptor(final Class clazz, final StringBuilder s Class currentClass = clazz; while (currentClass.isArray()) { stringBuilder.append('['); - currentClass = currentClass.getComponentType(); + currentClass = currentClass.componentType(); } if (currentClass.isPrimitive()) { char descriptor; @@ -702,6 +688,43 @@ public int getSize() { } } + /** + * Returns the number of arguments of this method type. This method should only be used for method + * types. + * + * @return the number of arguments of this method type. Each argument counts for 1, even long and + * double ones. The implicit @literal{this} argument is not counted. + */ + public int getArgumentCount() { + return getArgumentCount(getDescriptor()); + } + + /** + * Returns the number of arguments in the given method descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the number of arguments in the given method descriptor. Each argument counts for 1, + * even long and double ones. The implicit @literal{this} argument is not counted. + */ + public static int getArgumentCount(final String methodDescriptor) { + int argumentCount = 0; + // Skip the first character, which is always a '('. + int currentOffset = 1; + // Parse the argument types, one at a each loop iteration. + while (methodDescriptor.charAt(currentOffset) != ')') { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + ++argumentCount; + } + return argumentCount; + } + /** * Returns the size of the arguments and of the return value of methods of this type. This method * should only be used for method types. @@ -709,7 +732,8 @@ public int getSize() { * @return the size of the arguments of the method (plus one for the implicit this argument), * argumentsSize, and the size of its return value, returnSize, packed into a single int i = * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code - * i >> 2}, and returnSize to {@code i & 0x03}). + * i >> 2}, and returnSize to {@code i & 0x03}). Long and double values have size 2, + * the others have size 1. */ public int getArgumentsAndReturnSizes() { return getArgumentsAndReturnSizes(getDescriptor()); @@ -722,7 +746,8 @@ public int getArgumentsAndReturnSizes() { * @return the size of the arguments of the method (plus one for the implicit this argument), * argumentsSize, and the size of its return value, returnSize, packed into a single int i = * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code - * i >> 2}, and returnSize to {@code i & 0x03}). + * i >> 2}, and returnSize to {@code i & 0x03}). Long and double values have size 2, + * the others have size 1. */ public static int getArgumentsAndReturnSizes(final String methodDescriptor) { int argumentsSize = 1; diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java index 4551b16ebbec..7a2a3b6854b0 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java @@ -1,13 +1,13 @@ /* - * Copyright 2003,2004 The Apache Software Foundation + * Copyright 2002-2023 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); + * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software + * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.springframework.asm.ClassVisitor; @@ -288,7 +289,7 @@ public boolean equals(Object o) { } Object v1 = get(key); Object v2 = other.get(key); - if (!((v1 == null) ? v2 == null : v1.equals(v2))) { + if (!(Objects.equals(v1, v2))) { return false; } } diff --git a/spring-core/src/main/java/org/springframework/cglib/core/CollectionUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/CollectionUtils.java index 1590cad07797..0e6834382afd 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/CollectionUtils.java @@ -52,12 +52,7 @@ public static void reverse(Map source, Map target) { } public static Collection filter(Collection c, Predicate p) { - Iterator it = c.iterator(); - while (it.hasNext()) { - if (!p.evaluate(it.next())) { - it.remove(); - } - } + c.removeIf(o -> !p.evaluate(o)); return c; } diff --git a/spring-core/src/main/java/org/springframework/cglib/core/DefaultNamingPolicy.java b/spring-core/src/main/java/org/springframework/cglib/core/DefaultNamingPolicy.java index 9020e490abd6..06b14eb46b8f 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/DefaultNamingPolicy.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/DefaultNamingPolicy.java @@ -32,7 +32,7 @@ public class DefaultNamingPolicy implements NamingPolicy { /** * This allows to test collisions of {@code key.hashCode()}. */ - private final static boolean STRESS_HASH_CODE = Boolean.getBoolean("org.springframework.cglib.test.stressHashCodes"); + private static final boolean STRESS_HASH_CODE = Boolean.getBoolean("org.springframework.cglib.test.stressHashCodes"); @Override public String getClassName(String prefix, String source, Object key, Predicate names) { diff --git a/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java index a932f4f886d2..c1e157b72285 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java @@ -347,7 +347,7 @@ private static void load_class_helper(CodeEmitter e, final Type type) { public static void push_array(CodeEmitter e, Object[] array) { e.push(array.length); - e.newarray(Type.getType(remapComponentType(array.getClass().getComponentType()))); + e.newarray(Type.getType(remapComponentType(array.getClass().componentType()))); for (int i = 0; i < array.length; i++) { e.dup(); e.push(i); diff --git a/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java b/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java index 2287cb6144e9..9bee25e1c7ba 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java @@ -82,7 +82,7 @@ abstract public class KeyFactory { TypeUtils.parseSignature("int getSort()"); //generated numbers: - private final static int PRIMES[] = { + private static final int PRIMES[] = { 11, 73, 179, 331, 521, 787, 1213, 1823, 2609, 3691, 5189, 7247, diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index 9bd022ccee57..102f333c074b 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -75,7 +75,7 @@ private ReflectUtils() { Throwable throwable = null; try { classLoaderDefineClass = ClassLoader.class.getDeclaredMethod("defineClass", - String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class); + String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class); } catch (Throwable t) { classLoaderDefineClass = null; @@ -544,7 +544,15 @@ public String getMessage() { // No defineClass variant available at all? if (c == null) { - throw new CodeGenerationException(t); + throw new CodeGenerationException(t) { + @Override + public String getMessage() { + return "No compatible defineClass mechanism detected: " + + "JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + + "for ClassLoader.defineClass to be accessible. On the module path, " + + "you may not be able to define this CGLIB-generated class at all."; + } + }; } // Force static initializers to run. diff --git a/spring-core/src/main/java/org/springframework/cglib/core/internal/CustomizerRegistry.java b/spring-core/src/main/java/org/springframework/cglib/core/internal/CustomizerRegistry.java index b05afceab1dd..bba568fff7b4 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/internal/CustomizerRegistry.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/internal/CustomizerRegistry.java @@ -22,10 +22,7 @@ public void add(KeyFactoryCustomizer customizer) { Class klass = customizer.getClass(); for (Class type : customizerTypes) { if (type.isAssignableFrom(klass)) { - List list = customizers.get(type); - if (list == null) { - customizers.put(type, list = new ArrayList<>()); - } + List list = customizers.computeIfAbsent(type, k -> new ArrayList<>()); list.add(customizer); } } diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java index ca468ca4324a..de3e7de1b036 100644 --- a/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java @@ -57,7 +57,7 @@ public static MethodProxy create(Class c1, Class c2, String desc, String name1, proxy.createInfo = new CreateInfo(c1, c2); // SPRING PATCH BEGIN - if (!c1.isInterface() && c1 != Object.class && !Factory.class.isAssignableFrom(c2)) { + if (c1 != Object.class && c1.isAssignableFrom(c2.getSuperclass()) && !Factory.class.isAssignableFrom(c2)) { // Try early initialization for overridden methods on specifically purposed subclasses try { proxy.init(); diff --git a/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorter.java b/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorter.java index 11cdf02d8222..42f60ac8c1b0 100644 --- a/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorter.java +++ b/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorter.java @@ -154,7 +154,7 @@ public void mergeSort(int index, int lo, int hi, Comparator cmp) { private void chooseComparer(int index, Comparator cmp) { Object array = a[index]; - Class type = array.getClass().getComponentType(); + Class type = array.getClass().componentType(); if (type.equals(Integer.TYPE)) { comparer = new IntComparer((int[])array); } else if (type.equals(Long.TYPE)) { diff --git a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java index 8aa4380f874a..a5c666202515 100644 --- a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java +++ b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.core; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; @@ -50,43 +51,81 @@ */ public final class BridgeMethodResolver { - private static final Map cache = new ConcurrentReferenceHashMap<>(); + private static final Map cache = new ConcurrentReferenceHashMap<>(); private BridgeMethodResolver() { } /** - * Find the original method for the supplied {@link Method bridge Method}. + * Find the local original method for the supplied {@link Method bridge Method}. *

    It is safe to call this method passing in a non-bridge {@link Method} instance. * In such a case, the supplied {@link Method} instance is returned directly to the caller. * Callers are not required to check for bridging before calling this method. - * @param bridgeMethod the method to introspect + * @param bridgeMethod the method to introspect against its declaring class * @return the original method (either the bridged method or the passed-in method * if no more specific one could be found) + * @see #getMostSpecificMethod(Method, Class) */ public static Method findBridgedMethod(Method bridgeMethod) { - if (!bridgeMethod.isBridge()) { + return resolveBridgeMethod(bridgeMethod, bridgeMethod.getDeclaringClass()); + } + + /** + * Determine the most specific method for the supplied {@link Method bridge Method} + * in the given class hierarchy, even if not available on the local declaring class. + *

    This is effectively a combination of {@link ClassUtils#getMostSpecificMethod} + * and {@link #findBridgedMethod}, resolving the original method even if no bridge + * method has been generated at the same class hierarchy level (a known difference + * between the Eclipse compiler and regular javac). + * @param bridgeMethod the method to introspect against the given target class + * @param targetClass the target class to find the most specific method on + * @return the most specific method corresponding to the given bridge method + * (can be the original method if no more specific one could be found) + * @since 6.1.3 + * @see #findBridgedMethod + * @see org.springframework.util.ClassUtils#getMostSpecificMethod + */ + public static Method getMostSpecificMethod(Method bridgeMethod, @Nullable Class targetClass) { + if (targetClass != null && + !ClassUtils.getUserClass(bridgeMethod.getDeclaringClass()).isAssignableFrom(targetClass) && + !Proxy.isProxyClass(bridgeMethod.getDeclaringClass())) { + // From a different class hierarchy, and not a JDK or CGLIB proxy either -> return as-is. return bridgeMethod; } - Method bridgedMethod = cache.get(bridgeMethod); + + Method specificMethod = ClassUtils.getMostSpecificMethod(bridgeMethod, targetClass); + return resolveBridgeMethod(specificMethod, + (targetClass != null ? targetClass : specificMethod.getDeclaringClass())); + } + + private static Method resolveBridgeMethod(Method bridgeMethod, Class targetClass) { + boolean localBridge = (targetClass == bridgeMethod.getDeclaringClass()); + Class userClass = targetClass; + if (!bridgeMethod.isBridge() && localBridge) { + userClass = ClassUtils.getUserClass(targetClass); + if (userClass == targetClass) { + return bridgeMethod; + } + } + + Object cacheKey = (localBridge ? bridgeMethod : new MethodClassKey(bridgeMethod, targetClass)); + Method bridgedMethod = cache.get(cacheKey); if (bridgedMethod == null) { // Gather all methods with matching name and parameter size. List candidateMethods = new ArrayList<>(); - MethodFilter filter = candidateMethod -> - isBridgedCandidateFor(candidateMethod, bridgeMethod); - ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter); + MethodFilter filter = (candidateMethod -> isBridgedCandidateFor(candidateMethod, bridgeMethod)); + ReflectionUtils.doWithMethods(userClass, candidateMethods::add, filter); if (!candidateMethods.isEmpty()) { - bridgedMethod = candidateMethods.size() == 1 ? - candidateMethods.get(0) : - searchCandidates(candidateMethods, bridgeMethod); + bridgedMethod = (candidateMethods.size() == 1 ? candidateMethods.get(0) : + searchCandidates(candidateMethods, bridgeMethod)); } if (bridgedMethod == null) { // A bridge method was passed in but we couldn't find the bridged method. // Let's proceed with the passed-in method and hope for the best... bridgedMethod = bridgeMethod; } - cache.put(bridgeMethod, bridgedMethod); + cache.put(cacheKey, bridgedMethod); } return bridgedMethod; } @@ -121,8 +160,8 @@ private static Method searchCandidates(List candidateMethods, Method bri return candidateMethod; } else if (previousMethod != null) { - sameSig = sameSig && - Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); + sameSig = sameSig && Arrays.equals( + candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); } previousMethod = candidateMethod; } @@ -158,12 +197,13 @@ private static boolean isResolvedTypeMatch(Method genericMethod, Method candidat Class candidateParameter = candidateParameters[i]; if (candidateParameter.isArray()) { // An array type: compare the component type. - if (!candidateParameter.getComponentType().equals(genericParameter.getComponentType().toClass())) { + if (!candidateParameter.componentType().equals(genericParameter.getComponentType().toClass())) { return false; } } // A non-array type: compare the type itself. - if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { + if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals( + ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { return false; } } @@ -177,6 +217,10 @@ private static boolean isResolvedTypeMatch(Method genericMethod, Method candidat */ @Nullable private static Method findGenericDeclaration(Method bridgeMethod) { + if (!bridgeMethod.isBridge()) { + return bridgeMethod; + } + // Search parent types for method that has same signature as bridge. Class superclass = bridgeMethod.getDeclaringClass().getSuperclass(); while (superclass != null && Object.class != superclass) { @@ -226,14 +270,19 @@ private static Method searchForMatch(Class type, Method bridgeMethod) { /** * Compare the signatures of the bridge method and the method which it bridges. If * the parameter and return types are the same, it is a 'visibility' bridge method - * introduced in Java 6 to fix https://bugs.openjdk.org/browse/JDK-6342411. - * See also https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html + * introduced in Java 6 to fix + * JDK-6342411. * @return whether signatures match as described */ public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) { if (bridgeMethod == bridgedMethod) { + // Same method: for common purposes, return true to proceed as if it was a visibility bridge. return true; } + if (ClassUtils.getUserClass(bridgeMethod.getDeclaringClass()) != bridgeMethod.getDeclaringClass()) { + // Method on generated subclass: return false to consistently ignore it for visibility purposes. + return false; + } return (bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()) && bridgeMethod.getParameterCount() == bridgedMethod.getParameterCount() && Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes())); diff --git a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java index f15620cc7d2f..83af2d54dd4e 100644 --- a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java +++ b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java @@ -95,7 +95,9 @@ private CollectionFactory() { * @return {@code true} if the type is approximable */ public static boolean isApproximableCollectionType(@Nullable Class collectionType) { - return (collectionType != null && approximableCollectionTypes.contains(collectionType)); + return (collectionType != null && (approximableCollectionTypes.contains(collectionType) || + collectionType.getName().equals("java.util.SequencedSet") || + collectionType.getName().equals("java.util.SequencedCollection"))); } /** @@ -180,7 +182,9 @@ public static Collection createCollection(Class collectionType, int ca public static Collection createCollection(Class collectionType, @Nullable Class elementType, int capacity) { Assert.notNull(collectionType, "Collection type must not be null"); if (LinkedHashSet.class == collectionType || - Set.class == collectionType || Collection.class == collectionType) { + Set.class == collectionType || Collection.class == collectionType || + collectionType.getName().equals("java.util.SequencedSet") || + collectionType.getName().equals("java.util.SequencedCollection")) { return new LinkedHashSet<>(capacity); } else if (ArrayList.class == collectionType || List.class == collectionType) { @@ -221,7 +225,8 @@ else if (HashSet.class == collectionType) { * @return {@code true} if the type is approximable */ public static boolean isApproximableMapType(@Nullable Class mapType) { - return (mapType != null && approximableMapTypes.contains(mapType)); + return (mapType != null && (approximableMapTypes.contains(mapType) || + mapType.getName().equals("java.util.SequencedMap"))); } /** @@ -300,7 +305,8 @@ public static Map createMap(Class mapType, int capacity) { @SuppressWarnings({"rawtypes", "unchecked"}) public static Map createMap(Class mapType, @Nullable Class keyType, int capacity) { Assert.notNull(mapType, "Map type must not be null"); - if (LinkedHashMap.class == mapType || Map.class == mapType) { + if (LinkedHashMap.class == mapType || Map.class == mapType || + mapType.getName().equals("java.util.SequencedMap")) { return new LinkedHashMap<>(capacity); } else if (LinkedMultiValueMap.class == mapType || MultiValueMap.class == mapType) { diff --git a/spring-core/src/main/java/org/springframework/core/Constants.java b/spring-core/src/main/java/org/springframework/core/Constants.java index 1c949d19d70e..7e794fe4d288 100644 --- a/spring-core/src/main/java/org/springframework/core/Constants.java +++ b/spring-core/src/main/java/org/springframework/core/Constants.java @@ -32,7 +32,7 @@ * in public static final members. The {@code asXXXX} methods of this class * allow these constant values to be accessed via their string names. * - *

    Consider class Foo containing {@code public final static int CONSTANT1 = 66;} + *

    Consider class Foo containing {@code public static final int CONSTANT1 = 66;} * An instance of this class wrapping {@code Foo.class} will return the constant value * of 66 from its {@code asNumber} method given the argument {@code "CONSTANT1"}. * @@ -43,7 +43,10 @@ * @author Rod Johnson * @author Juergen Hoeller * @since 16.03.2003 + * @deprecated since 6.1 with no replacement; use an enum, map, or similar custom + * solution instead */ +@Deprecated(since = "6.1") public class Constants { /** The name of the introspected class. */ diff --git a/spring-core/src/main/java/org/springframework/core/Conventions.java b/spring-core/src/main/java/org/springframework/core/Conventions.java index 7eac92ca0d14..ba21b61158ee 100644 --- a/spring-core/src/main/java/org/springframework/core/Conventions.java +++ b/spring-core/src/main/java/org/springframework/core/Conventions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ public static String getVariableName(Object value) { boolean pluralize = false; if (value.getClass().isArray()) { - valueClass = value.getClass().getComponentType(); + valueClass = value.getClass().componentType(); pluralize = true; } else if (value instanceof Collection collection) { @@ -104,7 +104,7 @@ public static String getVariableNameForParameter(MethodParameter parameter) { String reactiveSuffix = ""; if (parameter.getParameterType().isArray()) { - valueClass = parameter.getParameterType().getComponentType(); + valueClass = parameter.getParameterType().componentType(); pluralize = true; } else if (Collection.class.isAssignableFrom(parameter.getParameterType())) { @@ -178,7 +178,7 @@ public static String getVariableNameForReturnType(Method method, Class resolv String reactiveSuffix = ""; if (resolvedType.isArray()) { - valueClass = resolvedType.getComponentType(); + valueClass = resolvedType.componentType(); pluralize = true; } else if (Collection.class.isAssignableFrom(resolvedType)) { diff --git a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java index 8b075798c0f2..125d5a6e4d0f 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,19 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Objects; +import java.util.Map; import kotlin.Unit; import kotlin.coroutines.CoroutineContext; import kotlin.jvm.JvmClassMappingKt; import kotlin.reflect.KClass; -import kotlin.reflect.KClassifier; import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.KType; import kotlin.reflect.full.KCallables; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.full.KClassifiers; +import kotlin.reflect.full.KTypes; import kotlin.reflect.jvm.KCallablesJvm; import kotlin.reflect.jvm.ReflectJvmMapping; import kotlinx.coroutines.BuildersKt; @@ -41,7 +45,9 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Utilities for working with Kotlin Coroutines. @@ -52,6 +58,13 @@ */ public abstract class CoroutinesUtils { + private static final KType flowType = KClassifiers.getStarProjectedType(JvmClassMappingKt.getKotlinClass(Flow.class)); + + private static final KType monoType = KClassifiers.getStarProjectedType(JvmClassMappingKt.getKotlinClass(Mono.class)); + + private static final KType publisherType = KClassifiers.getStarProjectedType(JvmClassMappingKt.getKotlinClass(Publisher.class)); + + /** * Convert a {@link Deferred} instance to a {@link Mono}. */ @@ -79,8 +92,7 @@ public static Deferred monoToDeferred(Mono source) { * @return the method invocation result as reactive stream * @throws IllegalArgumentException if {@code method} is not a suspending function */ - public static Publisher invokeSuspendingFunction(Method method, Object target, - Object... args) { + public static Publisher invokeSuspendingFunction(Method method, Object target, @Nullable Object... args) { return invokeSuspendingFunction(Dispatchers.getUnconfined(), method, target, args); } @@ -96,43 +108,59 @@ public static Publisher invokeSuspendingFunction(Method method, Object target * @throws IllegalArgumentException if {@code method} is not a suspending function * @since 6.0 */ - @SuppressWarnings("deprecation") - public static Publisher invokeSuspendingFunction(CoroutineContext context, Method method, Object target, - Object... args) { - Assert.isTrue(KotlinDetector.isSuspendingFunction(method), "'method' must be a suspending function"); - KFunction function = Objects.requireNonNull(ReflectJvmMapping.getKotlinFunction(method)); + @SuppressWarnings({"deprecation", "DataFlowIssue"}) + public static Publisher invokeSuspendingFunction( + CoroutineContext context, Method method, @Nullable Object target, @Nullable Object... args) { + + Assert.isTrue(KotlinDetector.isSuspendingFunction(method), "Method must be a suspending function"); + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + Assert.notNull(function, () -> "Failed to get Kotlin function for method: " + method); if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) { KCallablesJvm.setAccessible(function, true); } - Mono mono = MonoKt.mono(context, (scope, continuation) -> - KCallables.callSuspend(function, getSuspendedFunctionArgs(method, target, args), continuation)) - .filter(result -> !Objects.equals(result, Unit.INSTANCE)) + Mono mono = MonoKt.mono(context, (scope, continuation) -> { + Map argMap = CollectionUtils.newHashMap(args.length + 1); + int index = 0; + for (KParameter parameter : function.getParameters()) { + switch (parameter.getKind()) { + case INSTANCE -> argMap.put(parameter, target); + case VALUE, EXTENSION_RECEIVER -> { + Object arg = args[index]; + if (!(parameter.isOptional() && arg == null)) { + KType type = parameter.getType(); + if (!(type.isMarkedNullable() && arg == null) && + type.getClassifier() instanceof KClass kClass && + KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) { + KFunction constructor = KClasses.getPrimaryConstructor(kClass); + if (!KCallablesJvm.isAccessible(constructor)) { + KCallablesJvm.setAccessible(constructor, true); + } + arg = constructor.call(arg); + } + argMap.put(parameter, arg); + } + index++; + } + } + } + return KCallables.callSuspendBy(function, argMap, continuation); + }) + .filter(result -> result != Unit.INSTANCE) .onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException); - KClassifier returnType = function.getReturnType().getClassifier(); - if (returnType != null) { - if (returnType.equals(JvmClassMappingKt.getKotlinClass(Flow.class))) { - return mono.flatMapMany(CoroutinesUtils::asFlux); - } - else if (returnType.equals(JvmClassMappingKt.getKotlinClass(Mono.class))) { + KType returnType = function.getReturnType(); + if (KTypes.isSubtypeOf(returnType, flowType)) { + return mono.flatMapMany(CoroutinesUtils::asFlux); + } + if (KTypes.isSubtypeOf(returnType, publisherType)) { + if (KTypes.isSubtypeOf(returnType, monoType)) { return mono.flatMap(o -> ((Mono)o)); } - else if (returnType instanceof KClass kClass && - Publisher.class.isAssignableFrom(JvmClassMappingKt.getJavaClass(kClass))) { - return mono.flatMapMany(o -> ((Publisher)o)); - } + return mono.flatMapMany(o -> ((Publisher)o)); } return mono; } - private static Object[] getSuspendedFunctionArgs(Method method, Object target, Object... args) { - int length = (args.length == method.getParameterCount() - 1 ? args.length + 1 : args.length); - Object[] functionArgs = new Object[length]; - functionArgs[0] = target; - System.arraycopy(args, 0, functionArgs, 1, length - 1); - return functionArgs; - } - private static Flux asFlux(Object flow) { return ReactorFlowKt.asFlux(((Flow) flow)); } diff --git a/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java index b7bee1aea84f..18c39c1ad00b 100644 --- a/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ /** * Default implementation of the {@link ParameterNameDiscoverer} strategy interface, - * delegating to the Java 8 standard reflection mechanism, with a deprecated fallback - * to {@link LocalVariableTableParameterNameDiscoverer}. + * delegating to the Java 8 standard reflection mechanism. * *

    If a Kotlin reflection implementation is present, * {@link KotlinReflectionParameterNameDiscoverer} is added first in the list and @@ -36,7 +35,6 @@ */ public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { - @SuppressWarnings("removal") public DefaultParameterNameDiscoverer() { if (KotlinDetector.isKotlinReflectPresent()) { addDiscoverer(new KotlinReflectionParameterNameDiscoverer()); @@ -44,12 +42,6 @@ public DefaultParameterNameDiscoverer() { // Recommended approach on Java 8+: compilation with -parameters. addDiscoverer(new StandardReflectionParameterNameDiscoverer()); - - // Deprecated fallback to class file parsing for -debug symbols. - // Does not work on native images without class file resources. - if (!NativeDetector.inNativeImage()) { - addDiscoverer(new LocalVariableTableParameterNameDiscoverer()); - } } } diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java index 27f8682f474b..d08be27dcb4e 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -81,18 +81,18 @@ public static Class resolveReturnType(Method method, Class clazz) { } /** - * Resolve the single type argument of the given generic interface against the given - * target method which is assumed to return the given interface or an implementation + * Resolve the single type argument of the given generic type against the given + * target method which is assumed to return the given type or an implementation * of it. * @param method the target method to check the return type of - * @param genericIfc the generic interface or superclass to resolve the type argument from + * @param genericType the generic interface or superclass to resolve the type argument from * @return the resolved parameter type of the method return type, or {@code null} * if not resolvable or if the single argument is of type {@link WildcardType}. */ @Nullable - public static Class resolveReturnTypeArgument(Method method, Class genericIfc) { + public static Class resolveReturnTypeArgument(Method method, Class genericType) { Assert.notNull(method, "Method must not be null"); - ResolvableType resolvableType = ResolvableType.forMethodReturnType(method).as(genericIfc); + ResolvableType resolvableType = ResolvableType.forMethodReturnType(method).as(genericType); if (!resolvableType.hasGenerics() || resolvableType.getType() instanceof WildcardType) { return null; } @@ -100,16 +100,16 @@ public static Class resolveReturnTypeArgument(Method method, Class generic } /** - * Resolve the single type argument of the given generic interface against - * the given target class which is assumed to implement the generic interface + * Resolve the single type argument of the given generic type against + * the given target class which is assumed to implement the given type * and possibly declare a concrete type for its type variable. * @param clazz the target class to check against - * @param genericIfc the generic interface or superclass to resolve the type argument from + * @param genericType the generic interface or superclass to resolve the type argument from * @return the resolved type of the argument, or {@code null} if not resolvable */ @Nullable - public static Class resolveTypeArgument(Class clazz, Class genericIfc) { - ResolvableType resolvableType = ResolvableType.forClass(clazz).as(genericIfc); + public static Class resolveTypeArgument(Class clazz, Class genericType) { + ResolvableType resolvableType = ResolvableType.forClass(clazz).as(genericType); if (!resolvableType.hasGenerics()) { return null; } @@ -126,17 +126,17 @@ private static Class getSingleGeneric(ResolvableType resolvableType) { /** - * Resolve the type arguments of the given generic interface against the given - * target class which is assumed to implement the generic interface and possibly - * declare concrete types for its type variables. + * Resolve the type arguments of the given generic type against the given + * target class which is assumed to implement or extend from the given type + * and possibly declare concrete types for its type variables. * @param clazz the target class to check against - * @param genericIfc the generic interface or superclass to resolve the type argument from + * @param genericType the generic interface or superclass to resolve the type argument from * @return the resolved type of each argument, with the array size matching the * number of actual type arguments, or {@code null} if not resolvable */ @Nullable - public static Class[] resolveTypeArguments(Class clazz, Class genericIfc) { - ResolvableType type = ResolvableType.forClass(clazz).as(genericIfc); + public static Class[] resolveTypeArguments(Class clazz, Class genericType) { + ResolvableType type = ResolvableType.forClass(clazz).as(genericType); if (!type.hasGenerics() || type.isEntirelyUnresolvable()) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java index fde4e10a6c91..696ee372b00e 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,24 +35,34 @@ public abstract class KotlinDetector { @Nullable private static final Class kotlinMetadata; + @Nullable + private static final Class kotlinJvmInline; + // For ConstantFieldFeature compliance, otherwise could be deduced from kotlinMetadata private static final boolean kotlinPresent; private static final boolean kotlinReflectPresent; static { - Class metadata; ClassLoader classLoader = KotlinDetector.class.getClassLoader(); + Class metadata = null; + Class jvmInline = null; try { metadata = ClassUtils.forName("kotlin.Metadata", classLoader); + try { + jvmInline = ClassUtils.forName("kotlin.jvm.JvmInline", classLoader); + } + catch (ClassNotFoundException ex) { + // JVM inline support not available + } } catch (ClassNotFoundException ex) { // Kotlin API not available - no Kotlin support - metadata = null; } kotlinMetadata = (Class) metadata; kotlinPresent = (kotlinMetadata != null); - kotlinReflectPresent = ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); + kotlinReflectPresent = kotlinPresent && ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); + kotlinJvmInline = (Class) jvmInline; } @@ -74,6 +84,10 @@ public static boolean isKotlinReflectPresent() { /** * Determine whether the given {@code Class} is a Kotlin type * (with Kotlin metadata present on it). + * + *

    As of Kotlin 2.0, this method can't be used to detect Kotlin + * lambdas unless they are annotated with @JvmSerializableLambda + * as invokedynamic has become the default method for lambda generation. */ public static boolean isKotlinType(Class clazz) { return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null); @@ -93,4 +107,14 @@ public static boolean isSuspendingFunction(Method method) { return false; } + /** + * Determine whether the given {@code Class} is an inline class + * (annotated with {@code @JvmInline}). + * @since 6.1.5 + * @see Kotlin inline value classes + */ + public static boolean isInlineClass(Class clazz) { + return (kotlinJvmInline != null && clazz.getDeclaredAnnotation(kotlinJvmInline) != null); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java deleted file mode 100644 index 5d666a51f45d..000000000000 --- a/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.core; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Method; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.asm.ClassReader; -import org.springframework.asm.ClassVisitor; -import org.springframework.asm.Label; -import org.springframework.asm.MethodVisitor; -import org.springframework.asm.Opcodes; -import org.springframework.asm.SpringAsmInfo; -import org.springframework.asm.Type; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; - -/** - * Implementation of {@link ParameterNameDiscoverer} that uses the LocalVariableTable - * information in the method attributes to discover parameter names. Returns - * {@code null} if the class file was compiled without debug information. - * - *

    Uses ObjectWeb's ASM library for analyzing class files. Each discoverer instance - * caches the ASM discovered information for each introspected Class, in a thread-safe - * manner. It is recommended to reuse ParameterNameDiscoverer instances as far as possible. - * - *

    This class is deprecated in the 6.0 generation and scheduled for removal in 6.1 - * since it is effectively superseded by {@link StandardReflectionParameterNameDiscoverer}. - * For the time being, this discoverer logs a warning every time it actually inspects a - * class file which is particularly useful for identifying remaining gaps in usage of - * the standard "-parameters" compiler flag, and also unintended over-inspection of - * e.g. JDK core library classes (which are not compiled with the "-parameters" flag). - * - * @author Adrian Colyer - * @author Costin Leau - * @author Juergen Hoeller - * @author Chris Beams - * @author Sam Brannen - * @since 2.0 - * @see StandardReflectionParameterNameDiscoverer - * @see DefaultParameterNameDiscoverer - * @deprecated as of 6.0.1, in favor of {@link StandardReflectionParameterNameDiscoverer} - * (with the "-parameters" compiler flag) - */ -@Deprecated(since = "6.0.1", forRemoval = true) -public class LocalVariableTableParameterNameDiscoverer implements ParameterNameDiscoverer { - - private static final Log logger = LogFactory.getLog(LocalVariableTableParameterNameDiscoverer.class); - - // marker object for classes that do not have any debug info - private static final Map NO_DEBUG_INFO_MAP = Collections.emptyMap(); - - // the cache uses a nested index (value is a map) to keep the top level cache relatively small in size - private final Map, Map> parameterNamesCache = new ConcurrentHashMap<>(32); - - - @Override - @Nullable - public String[] getParameterNames(Method method) { - Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); - return doGetParameterNames(originalMethod); - } - - @Override - @Nullable - public String[] getParameterNames(Constructor ctor) { - return doGetParameterNames(ctor); - } - - @Nullable - private String[] doGetParameterNames(Executable executable) { - Class declaringClass = executable.getDeclaringClass(); - Map map = this.parameterNamesCache.computeIfAbsent(declaringClass, this::inspectClass); - return (map != NO_DEBUG_INFO_MAP ? map.get(executable) : null); - } - - /** - * Inspects the target class. - *

    Exceptions will be logged, and a marker map returned to indicate the - * lack of debug information. - */ - private Map inspectClass(Class clazz) { - InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz)); - if (is == null) { - // We couldn't load the class file, which is not fatal as it - // simply means this method of discovering parameter names won't work. - if (logger.isDebugEnabled()) { - logger.debug("Cannot find '.class' file for class [" + clazz + - "] - unable to determine constructor/method parameter names"); - } - return NO_DEBUG_INFO_MAP; - } - // We cannot use try-with-resources here for the InputStream, since we have - // custom handling of the close() method in a finally-block. - try { - ClassReader classReader = new ClassReader(is); - Map map = new ConcurrentHashMap<>(32); - classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0); - if (logger.isWarnEnabled()) { - logger.warn("Using deprecated '-debug' fallback for parameter name resolution. Compile the " + - "affected code with '-parameters' instead or avoid its introspection: " + clazz.getName()); - } - return map; - } - catch (IOException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Exception thrown while reading '.class' file for class [" + clazz + - "] - unable to determine constructor/method parameter names", ex); - } - } - catch (IllegalArgumentException ex) { - if (logger.isDebugEnabled()) { - logger.debug("ASM ClassReader failed to parse class file [" + clazz + - "], probably due to a new Java class file version that isn't supported yet " + - "- unable to determine constructor/method parameter names", ex); - } - } - finally { - try { - is.close(); - } - catch (IOException ex) { - // ignore - } - } - return NO_DEBUG_INFO_MAP; - } - - - /** - * Helper class that inspects all methods and constructors and then - * attempts to find the parameter names for the given {@link Executable}. - */ - private static class ParameterNameDiscoveringVisitor extends ClassVisitor { - - private static final String STATIC_CLASS_INIT = ""; - - private final Class clazz; - - private final Map executableMap; - - public ParameterNameDiscoveringVisitor(Class clazz, Map executableMap) { - super(SpringAsmInfo.ASM_VERSION); - this.clazz = clazz; - this.executableMap = executableMap; - } - - @Override - @Nullable - public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - // exclude synthetic + bridged && static class initialization - if (!isSyntheticOrBridged(access) && !STATIC_CLASS_INIT.equals(name)) { - return new LocalVariableTableVisitor(this.clazz, this.executableMap, name, desc, isStatic(access)); - } - return null; - } - - private static boolean isSyntheticOrBridged(int access) { - return (((access & Opcodes.ACC_SYNTHETIC) | (access & Opcodes.ACC_BRIDGE)) > 0); - } - - private static boolean isStatic(int access) { - return ((access & Opcodes.ACC_STATIC) > 0); - } - } - - - private static class LocalVariableTableVisitor extends MethodVisitor { - - private static final String CONSTRUCTOR = ""; - - private final Class clazz; - - private final Map executableMap; - - private final String name; - - private final Type[] args; - - private final String[] parameterNames; - - private final boolean isStatic; - - private boolean hasLvtInfo = false; - - /* - * The nth entry contains the slot index of the LVT table entry holding the - * argument name for the nth parameter. - */ - private final int[] lvtSlotIndex; - - public LocalVariableTableVisitor(Class clazz, Map map, String name, String desc, boolean isStatic) { - super(SpringAsmInfo.ASM_VERSION); - this.clazz = clazz; - this.executableMap = map; - this.name = name; - this.args = Type.getArgumentTypes(desc); - this.parameterNames = new String[this.args.length]; - this.isStatic = isStatic; - this.lvtSlotIndex = computeLvtSlotIndices(isStatic, this.args); - } - - @Override - public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) { - this.hasLvtInfo = true; - for (int i = 0; i < this.lvtSlotIndex.length; i++) { - if (this.lvtSlotIndex[i] == index) { - this.parameterNames[i] = name; - } - } - } - - @Override - public void visitEnd() { - if (this.hasLvtInfo || (this.isStatic && this.parameterNames.length == 0)) { - // visitLocalVariable will never be called for static no args methods - // which doesn't use any local variables. - // This means that hasLvtInfo could be false for that kind of methods - // even if the class has local variable info. - this.executableMap.put(resolveExecutable(), this.parameterNames); - } - } - - private Executable resolveExecutable() { - ClassLoader loader = this.clazz.getClassLoader(); - Class[] argTypes = new Class[this.args.length]; - for (int i = 0; i < this.args.length; i++) { - argTypes[i] = ClassUtils.resolveClassName(this.args[i].getClassName(), loader); - } - try { - if (CONSTRUCTOR.equals(this.name)) { - return this.clazz.getDeclaredConstructor(argTypes); - } - return this.clazz.getDeclaredMethod(this.name, argTypes); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException("Method [" + this.name + - "] was discovered in the .class file but cannot be resolved in the class object", ex); - } - } - - private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) { - int[] lvtIndex = new int[paramTypes.length]; - int nextIndex = (isStatic ? 0 : 1); - for (int i = 0; i < paramTypes.length; i++) { - lvtIndex[i] = nextIndex; - if (isWideType(paramTypes[i])) { - nextIndex += 2; - } - else { - nextIndex++; - } - } - return lvtIndex; - } - - private static boolean isWideType(Type aType) { - // float is not a wide type - return (aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE); - } - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java index 9f905cbda473..f1ceac4a7ccf 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ * * @author Juergen Hoeller * @author Rossen Stoyanchev + * @author Sam Brannen * @since 4.2.3 */ public final class MethodIntrospector { @@ -75,6 +76,7 @@ public static Map selectMethods(Class targetType, final Metada if (result != null) { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); if (bridgedMethod == specificMethod || bridgedMethod == method || + bridgedMethod.equals(specificMethod) || bridgedMethod.equals(method) || metadataLookup.inspect(bridgedMethod) == null) { methodMap.put(specificMethod, result); } diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index a97658608fe5..40388e4f8cbb 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -20,12 +20,16 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; +import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Predicate; @@ -93,7 +97,7 @@ public class MethodParameter { private volatile ParameterNameDiscoverer parameterNameDiscoverer; @Nullable - private volatile String parameterName; + volatile String parameterName; @Nullable private volatile MethodParameter nestedMethodParameter; @@ -856,6 +860,74 @@ private static int validateIndex(Executable executable, int parameterIndex) { return parameterIndex; } + /** + * Create a new MethodParameter for the given field-aware constructor, + * e.g. on a data class or record type. + *

    A field-aware method parameter will detect field annotations as well, + * as long as the field name matches the parameter name. + * @param ctor the Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @param fieldName the name of the underlying field, + * matching the constructor's parameter name + * @return the corresponding MethodParameter instance + * @since 6.1 + */ + public static MethodParameter forFieldAwareConstructor(Constructor ctor, int parameterIndex, String fieldName) { + return new FieldAwareConstructorParameter(ctor, parameterIndex, fieldName); + } + + + /** + * {@link MethodParameter} subclass which detects field annotations as well. + */ + private static class FieldAwareConstructorParameter extends MethodParameter { + + @Nullable + private volatile Annotation[] combinedAnnotations; + + public FieldAwareConstructorParameter(Constructor constructor, int parameterIndex, String fieldName) { + super(constructor, parameterIndex); + this.parameterName = fieldName; + } + + @Override + public Annotation[] getParameterAnnotations() { + String parameterName = this.parameterName; + Assert.state(parameterName != null, "Parameter name not initialized"); + + Annotation[] anns = this.combinedAnnotations; + if (anns == null) { + anns = super.getParameterAnnotations(); + try { + Field field = getDeclaringClass().getDeclaredField(parameterName); + Annotation[] fieldAnns = field.getAnnotations(); + if (fieldAnns.length > 0) { + List merged = new ArrayList<>(anns.length + fieldAnns.length); + merged.addAll(Arrays.asList(anns)); + for (Annotation fieldAnn : fieldAnns) { + boolean existingType = false; + for (Annotation ann : anns) { + if (ann.annotationType() == fieldAnn.annotationType()) { + existingType = true; + break; + } + } + if (!existingType) { + merged.add(fieldAnn); + } + } + anns = merged.toArray(new Annotation[0]); + } + } + catch (NoSuchFieldException | SecurityException ex) { + // ignore + } + this.combinedAnnotations = anns; + } + return anns; + } + } + /** * Inner class to avoid a hard dependency on Kotlin at runtime. diff --git a/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java index 3d211a82d02f..ada54b517426 100644 --- a/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java +++ b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.core; +import java.util.Objects; +import java.util.function.Supplier; + import org.springframework.util.Assert; /** @@ -23,6 +26,7 @@ * as {@link #toString()} result (allowing for introspection). * * @author Juergen Hoeller + * @author Qimiao Chen * @since 2.5.2 * @param the value type * @see NamedInheritableThreadLocal @@ -46,4 +50,39 @@ public String toString() { return this.name; } + + /** + * Create a named thread local variable. The initial value of the variable is + * determined by invoking the {@code get} method on the {@code Supplier}. + * @param the type of the named thread local's value + * @param name a descriptive name for the thread local + * @param supplier the supplier to be used to determine the initial value + * @return a new named thread local + * @since 6.1 + */ + public static ThreadLocal withInitial(String name, Supplier supplier) { + return new SuppliedNamedThreadLocal<>(name, supplier); + } + + + /** + * An extension of NamedThreadLocal that obtains its initial value from + * the specified {@code Supplier}. + * @param the type of the named thread local's value + */ + private static final class SuppliedNamedThreadLocal extends NamedThreadLocal { + + private final Supplier supplier; + + SuppliedNamedThreadLocal(String name, Supplier supplier) { + super(name); + this.supplier = Objects.requireNonNull(supplier); + } + + @Override + protected T initialValue() { + return this.supplier.get(); + } + } + } diff --git a/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java index ab0eefb213a8..bd6afe4d14cc 100644 --- a/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java +++ b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java @@ -41,7 +41,7 @@ public abstract class NestedRuntimeException extends RuntimeException { * Construct a {@code NestedRuntimeException} with the specified detail message. * @param msg the detail message */ - public NestedRuntimeException(String msg) { + public NestedRuntimeException(@Nullable String msg) { super(msg); } diff --git a/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java b/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java index 2090cd35f9cf..aac791637098 100644 --- a/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java +++ b/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,6 @@ * @author Sam Brannen * @since 2.5 * @see org.springframework.beans.factory.config.PropertyOverrideConfigurer - * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer */ public interface PriorityOrdered extends Ordered { } diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index e1aa6db3aaca..994b6e036e17 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java @@ -16,14 +16,17 @@ package org.springframework.core; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.Flow; import java.util.function.Function; +import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; import reactor.adapter.JdkFlowAdapter; import reactor.blockhound.BlockHound; @@ -34,6 +37,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; /** * A registry of adapters to adapt Reactive Streams {@link Publisher} to/from various @@ -43,7 +47,8 @@ * *

    By default, depending on classpath availability, adapters are registered for Reactor * (including {@code CompletableFuture} and {@code Flow.Publisher} adapters), RxJava 3, - * Kotlin Coroutines' {@code Deferred} (bridged via Reactor) and SmallRye Mutiny 1.x. + * Kotlin Coroutines' {@code Deferred} (bridged via Reactor) and SmallRye Mutiny 1.x/2.x. + * If Reactor is not present, a simple {@code Flow.Publisher} bridge will be registered. * * @author Rossen Stoyanchev * @author Sebastien Deleuze @@ -55,6 +60,8 @@ public class ReactiveAdapterRegistry { @Nullable private static volatile ReactiveAdapterRegistry sharedInstance; + private static final boolean reactiveStreamsPresent; + private static final boolean reactorPresent; private static final boolean rxjava3Present; @@ -65,6 +72,7 @@ public class ReactiveAdapterRegistry { static { ClassLoader classLoader = ReactiveAdapterRegistry.class.getClassLoader(); + reactiveStreamsPresent = ClassUtils.isPresent("org.reactivestreams.Publisher", classLoader); reactorPresent = ClassUtils.isPresent("reactor.core.publisher.Flux", classLoader); rxjava3Present = ClassUtils.isPresent("io.reactivex.rxjava3.core.Flowable", classLoader); kotlinCoroutinesPresent = ClassUtils.isPresent("kotlinx.coroutines.reactor.MonoKt", classLoader); @@ -79,6 +87,11 @@ public class ReactiveAdapterRegistry { * @see #getSharedInstance() */ public ReactiveAdapterRegistry() { + // Defensive guard for the Reactive Streams API itself + if (!reactiveStreamsPresent) { + return; + } + // Reactor if (reactorPresent) { new ReactorRegistrar().registerAdapters(this); @@ -98,6 +111,11 @@ public ReactiveAdapterRegistry() { if (mutinyPresent) { new MutinyRegistrar().registerAdapters(this); } + + // Simple Flow.Publisher bridge if Reactor is not present + if (!reactorPresent) { + new FlowAdaptersRegistrar().registerAdapters(this); + } } @@ -347,20 +365,68 @@ void registerAdapters(ReactiveAdapterRegistry registry) { private static class MutinyRegistrar { + private static final Method uniToPublisher = ClassUtils.getMethod(io.smallrye.mutiny.groups.UniConvert.class, "toPublisher"); + + @SuppressWarnings("unchecked") void registerAdapters(ReactiveAdapterRegistry registry) { - registry.registerReactiveType( - ReactiveTypeDescriptor.singleOptionalValue( - io.smallrye.mutiny.Uni.class, - () -> io.smallrye.mutiny.Uni.createFrom().nothing()), - uni -> ((io.smallrye.mutiny.Uni) uni).convert().toPublisher(), - publisher -> io.smallrye.mutiny.Uni.createFrom().publisher(publisher)); + ReactiveTypeDescriptor uniDesc = ReactiveTypeDescriptor.singleOptionalValue( + io.smallrye.mutiny.Uni.class, + () -> io.smallrye.mutiny.Uni.createFrom().nothing()); + ReactiveTypeDescriptor multiDesc = ReactiveTypeDescriptor.multiValue( + io.smallrye.mutiny.Multi.class, + () -> io.smallrye.mutiny.Multi.createFrom().empty()); + + if (Flow.Publisher.class.isAssignableFrom(uniToPublisher.getReturnType())) { + // Mutiny 2 based on Flow.Publisher + Method uniPublisher = ClassUtils.getMethod( + io.smallrye.mutiny.groups.UniCreate.class, "publisher", Flow.Publisher.class); + Method multiPublisher = ClassUtils.getMethod( + io.smallrye.mutiny.groups.MultiCreate.class, "publisher", Flow.Publisher.class); + registry.registerReactiveType(uniDesc, + uni -> FlowAdapters.toPublisher((Flow.Publisher) Objects.requireNonNull( + ReflectionUtils.invokeMethod(uniToPublisher, ((io.smallrye.mutiny.Uni) uni).convert()))), + publisher -> ReflectionUtils.invokeMethod(uniPublisher, io.smallrye.mutiny.Uni.createFrom(), + FlowAdapters.toFlowPublisher(publisher))); + registry.registerReactiveType(multiDesc, + multi -> FlowAdapters.toPublisher((Flow.Publisher) multi), + publisher -> ReflectionUtils.invokeMethod(multiPublisher, io.smallrye.mutiny.Multi.createFrom(), + FlowAdapters.toFlowPublisher(publisher))); + } + else { + // Mutiny 1 based on Reactive Streams + registry.registerReactiveType(uniDesc, + uni -> ((io.smallrye.mutiny.Uni) uni).convert().toPublisher(), + publisher -> io.smallrye.mutiny.Uni.createFrom().publisher(publisher)); + registry.registerReactiveType(multiDesc, + multi -> (io.smallrye.mutiny.Multi) multi, + publisher -> io.smallrye.mutiny.Multi.createFrom().publisher(publisher)); + } + } + } + + + private static class FlowAdaptersRegistrar { + + private static final Flow.Subscription EMPTY_SUBSCRIPTION = new Flow.Subscription() { + @Override + public void request(long n) { + } + @Override + public void cancel() { + } + }; + + private static final Flow.Publisher EMPTY_PUBLISHER = subscriber -> { + subscriber.onSubscribe(EMPTY_SUBSCRIPTION); + subscriber.onComplete(); + }; + @SuppressWarnings("unchecked") + void registerAdapters(ReactiveAdapterRegistry registry) { registry.registerReactiveType( - ReactiveTypeDescriptor.multiValue( - io.smallrye.mutiny.Multi.class, - () -> io.smallrye.mutiny.Multi.createFrom().empty()), - multi -> (io.smallrye.mutiny.Multi) multi, - publisher -> io.smallrye.mutiny.Multi.createFrom().publisher(publisher)); + ReactiveTypeDescriptor.multiValue(Flow.Publisher.class, () -> EMPTY_PUBLISHER), + source -> FlowAdapters.toPublisher((Flow.Publisher) source), + source -> FlowAdapters.toFlowPublisher((Publisher) source)); } } diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java index 5e37afabf097..433e2bf3b715 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,7 +98,9 @@ public boolean supportsEmpty() { */ public Object getEmptyValue() { Assert.state(this.emptySupplier != null, "Empty values not supported"); - return this.emptySupplier.get(); + Object emptyValue = this.emptySupplier.get(); + Assert.notNull(emptyValue, "Invalid null return value from emptySupplier"); + return emptyValue; } /** @@ -130,7 +132,7 @@ public int hashCode() { /** - * Descriptor for a reactive type that can produce 0..N values. + * Descriptor for a reactive type that can produce {@code 0..N} values. * @param type the reactive type * @param emptySupplier a supplier of an empty-value instance of the reactive type */ diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index c7403097d30b..46acaf778919 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,22 +101,22 @@ public class ResolvableType implements Serializable { private final Type type; /** - * Optional provider for the type. + * The component type for an array or {@code null} if the type should be deduced. */ @Nullable - private final TypeProvider typeProvider; + private final ResolvableType componentType; /** - * The {@code VariableResolver} to use or {@code null} if no resolver is available. + * Optional provider for the type. */ @Nullable - private final VariableResolver variableResolver; + private final TypeProvider typeProvider; /** - * The component type for an array or {@code null} if the type should be deduced. + * The {@code VariableResolver} to use or {@code null} if no resolver is available. */ @Nullable - private final ResolvableType componentType; + private final VariableResolver variableResolver; @Nullable private final Integer hash; @@ -145,9 +145,9 @@ private ResolvableType( Type type, @Nullable TypeProvider typeProvider, @Nullable VariableResolver variableResolver) { this.type = type; + this.componentType = null; this.typeProvider = typeProvider; this.variableResolver = variableResolver; - this.componentType = null; this.hash = calculateHashCode(); this.resolved = null; } @@ -161,9 +161,9 @@ private ResolvableType(Type type, @Nullable TypeProvider typeProvider, @Nullable VariableResolver variableResolver, @Nullable Integer hash) { this.type = type; + this.componentType = null; this.typeProvider = typeProvider; this.variableResolver = variableResolver; - this.componentType = null; this.hash = hash; this.resolved = resolveClass(); } @@ -172,13 +172,13 @@ private ResolvableType(Type type, @Nullable TypeProvider typeProvider, * Private constructor used to create a new {@code ResolvableType} for uncached purposes, * with upfront resolution but lazily calculated hash. */ - private ResolvableType(Type type, @Nullable TypeProvider typeProvider, - @Nullable VariableResolver variableResolver, @Nullable ResolvableType componentType) { + private ResolvableType(Type type, @Nullable ResolvableType componentType, + @Nullable TypeProvider typeProvider, @Nullable VariableResolver variableResolver) { this.type = type; + this.componentType = componentType; this.typeProvider = typeProvider; this.variableResolver = variableResolver; - this.componentType = componentType; this.hash = null; this.resolved = resolveClass(); } @@ -191,9 +191,9 @@ private ResolvableType(Type type, @Nullable TypeProvider typeProvider, private ResolvableType(@Nullable Class clazz) { this.resolved = (clazz != null ? clazz : Object.class); this.type = this.resolved; + this.componentType = null; this.typeProvider = null; this.variableResolver = null; - this.componentType = null; this.hash = null; } @@ -262,7 +262,9 @@ public boolean isInstance(@Nullable Object obj) { * @see #isAssignableFrom(ResolvableType) */ public boolean isAssignableFrom(Class other) { - return isAssignableFrom(forClass(other), null); + // As of 6.1: shortcut assignability check for top-level Class references + return (this.type instanceof Class clazz ? ClassUtils.isAssignable(clazz, other) : + isAssignableFrom(forClass(other), false, null)); } /** @@ -277,10 +279,10 @@ public boolean isAssignableFrom(Class other) { * {@code ResolvableType}; {@code false} otherwise */ public boolean isAssignableFrom(ResolvableType other) { - return isAssignableFrom(other, null); + return isAssignableFrom(other, false, null); } - private boolean isAssignableFrom(ResolvableType other, @Nullable Map matchedBefore) { + private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable Map matchedBefore) { Assert.notNull(other, "ResolvableType must not be null"); // If we cannot resolve types, we are not assignable @@ -288,13 +290,21 @@ private boolean isAssignableFrom(ResolvableType other, @Nullable Map return false; } - // Deal with array by delegating to the component type - if (isArray()) { - return (other.isArray() && getComponentType().isAssignableFrom(other.getComponentType())); + if (matchedBefore != null) { + if (matchedBefore.get(this.type) == other.type) { + return true; + } + } + else { + // As of 6.1: shortcut assignability check for top-level Class references + if (this.type instanceof Class clazz && other.type instanceof Class otherClazz) { + return (strict ? clazz.isAssignableFrom(otherClazz) : ClassUtils.isAssignable(clazz, otherClazz)); + } } - if (matchedBefore != null && matchedBefore.get(this.type) == other.type) { - return true; + // Deal with array by delegating to the component type + if (isArray()) { + return (other.isArray() && getComponentType().isAssignableFrom(other.getComponentType(), true, matchedBefore)); } // Deal with wildcard bounds @@ -347,7 +357,8 @@ private boolean isAssignableFrom(ResolvableType other, @Nullable Map // We need an exact type match for generics // List is not assignable from List if (exactMatch ? !ourResolved.equals(otherResolved) : - !ClassUtils.isAssignable(ourResolved, otherResolved)) { + (strict ? !ourResolved.isAssignableFrom(otherResolved) : + !ClassUtils.isAssignable(ourResolved, otherResolved))) { return false; } @@ -364,7 +375,7 @@ private boolean isAssignableFrom(ResolvableType other, @Nullable Map } matchedBefore.put(this.type, other.type); for (int i = 0; i < ourGenerics.length; i++) { - if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], matchedBefore)) { + if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore)) { return false; } } @@ -399,7 +410,7 @@ public ResolvableType getComponentType() { return this.componentType; } if (this.type instanceof Class clazz) { - Class componentType = clazz.getComponentType(); + Class componentType = clazz.componentType(); return forType(componentType, this.variableResolver); } if (this.type instanceof GenericArrayType genericArrayType) { @@ -647,7 +658,7 @@ public ResolvableType getNested(int nestingLevel) { * @param nestingLevel the required nesting level, indexed from 1 for the * current type, 2 for the first nested generic, 3 for the second and so on * @param typeIndexesPerLevel a map containing the generic index for a given - * nesting level (may be {@code null}) + * nesting level (can be {@code null}) * @return a {@code ResolvableType} for the nested level, or {@link #NONE} */ public ResolvableType getNested(int nestingLevel, @Nullable Map typeIndexesPerLevel) { @@ -679,7 +690,7 @@ public ResolvableType getNested(int nestingLevel, @Nullable MapIf no generic is available at the specified indexes {@link #NONE} is returned. * @param indexes the indexes that refer to the generic parameter - * (may be omitted to return the first generic) + * (can be omitted to return the first generic) * @return a {@code ResolvableType} for the specified generic, or {@link #NONE} * @see #hasGenerics() * @see #getGenerics() @@ -747,7 +758,7 @@ else if (this.type instanceof ParameterizedType parameterizedType) { * Convenience method that will {@link #getGenerics() get} and * {@link #resolve() resolve} generic parameters. * @return an array of resolved generic parameters (the resulting array - * will never be {@code null}, but it may contain {@code null} elements}) + * will never be {@code null}, but it may contain {@code null} elements) * @see #getGenerics() * @see #resolve() */ @@ -782,7 +793,7 @@ public Class[] resolveGenerics(Class fallback) { * Convenience method that will {@link #getGeneric(int...) get} and * {@link #resolve() resolve} a specific generic parameter. * @param indexes the indexes that refer to the generic parameter - * (may be omitted to return the first generic) + * (can be omitted to return the first generic) * @return a resolved {@link Class} or {@code null} * @see #getGeneric(int...) * @see #resolve() @@ -913,16 +924,22 @@ private ResolvableType resolveVariable(TypeVariable variable) { } + /** + * Check for full equality of all type resolution artifacts: + * type as well as {@code TypeProvider} and {@code VariableResolver}. + * @see #equalsType(ResolvableType) + */ @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof ResolvableType otherType)) { + if (other == null || other.getClass() != getClass()) { return false; } + ResolvableType otherType = (ResolvableType) other; - if (!ObjectUtils.nullSafeEquals(this.type, otherType.type)) { + if (!equalsType(otherType)) { return false; } if (this.typeProvider != otherType.typeProvider && @@ -935,12 +952,22 @@ public boolean equals(@Nullable Object other) { !ObjectUtils.nullSafeEquals(this.variableResolver.getSource(), otherType.variableResolver.getSource()))) { return false; } - if (!ObjectUtils.nullSafeEquals(this.componentType, otherType.componentType)) { - return false; - } return true; } + /** + * Check for type-level equality with another {@code ResolvableType}. + *

    In contrast to {@link #equals(Object)} or {@link #isAssignableFrom(ResolvableType)}, + * this works between different sources as well, e.g. method parameters and return types. + * @param otherType the {@code ResolvableType} to match against + * @return whether the declared type and type variables match + * @since 6.1 + */ + public boolean equalsType(ResolvableType otherType) { + return (ObjectUtils.nullSafeEquals(this.type, otherType.type) && + ObjectUtils.nullSafeEquals(this.componentType, otherType.componentType)); + } + @Override public int hashCode() { return (this.hash != null ? this.hash : calculateHashCode()); @@ -948,15 +975,15 @@ public int hashCode() { private int calculateHashCode() { int hashCode = ObjectUtils.nullSafeHashCode(this.type); + if (this.componentType != null) { + hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.componentType); + } if (this.typeProvider != null) { hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.typeProvider.getType()); } if (this.variableResolver != null) { hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.variableResolver.getSource()); } - if (this.componentType != null) { - hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.componentType); - } return hashCode; } @@ -1091,21 +1118,21 @@ public static ResolvableType forClassWithGenerics(Class clazz, Class... ge * @return a {@code ResolvableType} for the specific class and generics * @see #forClassWithGenerics(Class, Class...) */ - public static ResolvableType forClassWithGenerics(Class clazz, ResolvableType... generics) { + public static ResolvableType forClassWithGenerics(Class clazz, @Nullable ResolvableType... generics) { Assert.notNull(clazz, "Class must not be null"); - Assert.notNull(generics, "Generics array must not be null"); TypeVariable[] variables = clazz.getTypeParameters(); - Assert.isTrue(variables.length == generics.length, () -> "Mismatched number of generics specified for " + clazz.toGenericString()); - - Type[] arguments = new Type[generics.length]; - for (int i = 0; i < generics.length; i++) { - ResolvableType generic = generics[i]; + if (generics != null) { + Assert.isTrue(variables.length == generics.length, + () -> "Mismatched number of generics specified for " + clazz.toGenericString()); + } + Type[] arguments = new Type[variables.length]; + for (int i = 0; i < variables.length; i++) { + ResolvableType generic = (generics != null ? generics[i] : null); Type argument = (generic != null ? generic.getType() : null); arguments[i] = (argument != null && !(argument instanceof TypeVariable) ? argument : variables[i]); } - - ParameterizedType syntheticType = new SyntheticParameterizedType(clazz, arguments); - return forType(syntheticType, new TypeVariablesVariableResolver(variables, generics)); + return forType(new SyntheticParameterizedType(clazz, arguments), + (generics != null ? new TypeVariablesVariableResolver(variables, generics) : null)); } /** @@ -1360,8 +1387,8 @@ static ResolvableType forMethodParameter( */ public static ResolvableType forArrayComponent(ResolvableType componentType) { Assert.notNull(componentType, "Component type must not be null"); - Class arrayClass = Array.newInstance(componentType.resolve(), 0).getClass(); - return new ResolvableType(arrayClass, null, null, componentType); + Class arrayType = componentType.toClass().arrayType(); + return new ResolvableType(arrayType, componentType, null, null); } /** @@ -1437,7 +1464,7 @@ static ResolvableType forType( // For simple Class references, build the wrapper right away - // no expensive resolution necessary, so not worth caching... if (type instanceof Class) { - return new ResolvableType(type, typeProvider, variableResolver, (ResolvableType) null); + return new ResolvableType(type, null, typeProvider, variableResolver); } // Purge empty entries on access since we don't have a clean-up thread or the like. diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java index 01e5a42b5ec7..76bf2678d071 100644 --- a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -159,7 +159,7 @@ interface TypeProvider extends Serializable { /** * Return the source of the type, or {@code null} if not known. - *

    The default implementations returns {@code null}. + *

    The default implementation returns {@code null}. */ @Nullable default Object getSource() { @@ -186,17 +186,20 @@ public TypeProxyInvocationHandler(TypeProvider provider) { @Nullable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { switch (method.getName()) { - case "equals": + case "equals" -> { Object other = args[0]; // Unwrap proxies for speed if (other instanceof Type otherType) { other = unwrap(otherType); } return ObjectUtils.nullSafeEquals(this.provider.getType(), other); - case "hashCode": + } + case "hashCode" -> { return ObjectUtils.nullSafeHashCode(this.provider.getType()); - case "getTypeProvider": + } + case "getTypeProvider" -> { return this.provider; + } } if (Type.class == method.getReturnType() && ObjectUtils.isEmpty(args)) { @@ -214,7 +217,12 @@ else if (Type[].class == method.getReturnType() && ObjectUtils.isEmpty(args)) { return result; } - return ReflectionUtils.invokeMethod(method, this.provider.getType(), args); + Type type = this.provider.getType(); + if (type instanceof TypeVariable tv && method.getName().equals("getName")) { + // Avoid reflection for common comparison of type variables + return tv.getName(); + } + return ReflectionUtils.invokeMethod(method, type, args); } } diff --git a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java index 15c93e742639..a9d84542f03e 100644 --- a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ * * @author Juergen Hoeller * @author Qimiao Chen + * @author Sam Brannen * @since 2.5.2 */ public class SimpleAliasRegistry implements AliasRegistry { @@ -172,7 +173,7 @@ else if (!resolvedAlias.equals(alias)) { throw new IllegalStateException( "Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias + "') for name '" + resolvedName + "': It is already registered for name '" + - registeredName + "'."); + existingName + "'."); } checkForAliasCircle(resolvedName, resolvedAlias); this.aliasMap.remove(alias); diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java index 9c53470a8232..3cb1cd51ebf5 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -39,7 +39,6 @@ * @author Juergen Hoeller * @since 3.2.7 * @see org.springframework.beans.StandardBeanInfoFactory#IGNORE_BEANINFO_PROPERTY_NAME - * @see org.springframework.context.index.CandidateComponentsIndexLoader#IGNORE_INDEX * @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME * @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME * @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java index 63c7b05a7a29..978e9684c25a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; -import java.lang.reflect.Array; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Predicate; @@ -164,8 +163,7 @@ public > E getEnum(String attributeName, Class type) { @SuppressWarnings("unchecked") public > E[] getEnumArray(String attributeName, Class type) { Assert.notNull(type, "Type must not be null"); - Class arrayType = Array.newInstance(type, 0).getClass(); - return (E[]) getRequiredAttributeValue(attributeName, arrayType); + return (E[]) getRequiredAttributeValue(attributeName, type.arrayType()); } @Override diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java new file mode 100644 index 000000000000..e50e79bf9004 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java @@ -0,0 +1,362 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A convenient wrapper for a {@link Method} handle, providing deep annotation + * introspection on methods and method parameters, including the exposure of + * interface-declared parameter annotations from the concrete target method. + * + * @author Juergen Hoeller + * @since 6.1 + * @see #getMethodAnnotation(Class) + * @see #getMethodParameters() + * @see AnnotatedElementUtils + * @see SynthesizingMethodParameter + */ +public class AnnotatedMethod { + + private final Method method; + + private final Method bridgedMethod; + + private final MethodParameter[] parameters; + + @Nullable + private volatile List inheritedParameterAnnotations; + + + /** + * Create an instance that wraps the given {@link Method}. + * @param method the {@code Method} handle to wrap + */ + public AnnotatedMethod(Method method) { + Assert.notNull(method, "Method is required"); + this.method = method; + this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + ReflectionUtils.makeAccessible(this.bridgedMethod); + this.parameters = initMethodParameters(); + } + + /** + * Copy constructor for use in subclasses. + */ + protected AnnotatedMethod(AnnotatedMethod annotatedMethod) { + Assert.notNull(annotatedMethod, "AnnotatedMethod is required"); + this.method = annotatedMethod.method; + this.bridgedMethod = annotatedMethod.bridgedMethod; + this.parameters = annotatedMethod.parameters; + this.inheritedParameterAnnotations = annotatedMethod.inheritedParameterAnnotations; + } + + + /** + * Return the annotated method. + */ + public final Method getMethod() { + return this.method; + } + + /** + * If the annotated method is a bridge method, this method returns the bridged + * (user-defined) method. Otherwise, it returns the same method as {@link #getMethod()}. + */ + protected final Method getBridgedMethod() { + return this.bridgedMethod; + } + + /** + * Expose the containing class for method parameters. + * @see MethodParameter#getContainingClass() + */ + protected Class getContainingClass() { + return this.method.getDeclaringClass(); + } + + /** + * Return the method parameters for this {@code AnnotatedMethod}. + */ + public final MethodParameter[] getMethodParameters() { + return this.parameters; + } + + private MethodParameter[] initMethodParameters() { + int count = this.bridgedMethod.getParameterCount(); + MethodParameter[] result = new MethodParameter[count]; + for (int i = 0; i < count; i++) { + result[i] = new AnnotatedMethodParameter(i); + } + return result; + } + + /** + * Return a {@link MethodParameter} for the declared return type. + */ + public MethodParameter getReturnType() { + return new AnnotatedMethodParameter(-1); + } + + /** + * Return a {@link MethodParameter} for the actual return value type. + */ + public MethodParameter getReturnValueType(@Nullable Object returnValue) { + return new ReturnValueMethodParameter(returnValue); + } + + /** + * Return {@code true} if the method's return type is void, {@code false} otherwise. + */ + public boolean isVoid() { + return (getReturnType().getParameterType() == void.class); + } + + /** + * Return a single annotation on the underlying method, traversing its super methods + * if no annotation can be found on the given method itself. + *

    Supports merged composed annotations with attribute overrides. + * @param annotationType the annotation type to look for + * @return the annotation, or {@code null} if none found + * @see AnnotatedElementUtils#findMergedAnnotation + */ + @Nullable + public A getMethodAnnotation(Class annotationType) { + return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); + } + + /** + * Determine if an annotation of the given type is present or + * meta-present on the method. + * @param annotationType the annotation type to look for + * @see AnnotatedElementUtils#hasAnnotation + */ + public boolean hasMethodAnnotation(Class annotationType) { + return AnnotatedElementUtils.hasAnnotation(this.method, annotationType); + } + + private List getInheritedParameterAnnotations() { + List parameterAnnotations = this.inheritedParameterAnnotations; + if (parameterAnnotations == null) { + parameterAnnotations = new ArrayList<>(); + Class clazz = this.method.getDeclaringClass(); + while (clazz != null) { + for (Class ifc : clazz.getInterfaces()) { + for (Method candidate : ifc.getMethods()) { + if (isOverrideFor(candidate)) { + parameterAnnotations.add(candidate.getParameterAnnotations()); + } + } + } + clazz = clazz.getSuperclass(); + if (clazz == Object.class) { + clazz = null; + } + if (clazz != null) { + for (Method candidate : clazz.getMethods()) { + if (isOverrideFor(candidate)) { + parameterAnnotations.add(candidate.getParameterAnnotations()); + } + } + } + } + this.inheritedParameterAnnotations = parameterAnnotations; + } + return parameterAnnotations; + } + + private boolean isOverrideFor(Method candidate) { + if (!candidate.getName().equals(this.method.getName()) || + candidate.getParameterCount() != this.method.getParameterCount()) { + return false; + } + Class[] paramTypes = this.method.getParameterTypes(); + if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) { + return true; + } + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i] != + ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) { + return false; + } + } + return true; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other != null && getClass() == other.getClass() && + this.method.equals(((AnnotatedMethod) other).method))); + } + + @Override + public int hashCode() { + return this.method.hashCode(); + } + + @Override + public String toString() { + return this.method.toGenericString(); + } + + + // Support methods for use in subclass variants + + @Nullable + protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { + if (!ObjectUtils.isEmpty(providedArgs)) { + for (Object providedArg : providedArgs) { + if (parameter.getParameterType().isInstance(providedArg)) { + return providedArg; + } + } + } + return null; + } + + protected static String formatArgumentError(MethodParameter param, String message) { + return "Could not resolve parameter [" + param.getParameterIndex() + "] in " + + param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : ""); + } + + + /** + * A MethodParameter with AnnotatedMethod-specific behavior. + */ + protected class AnnotatedMethodParameter extends SynthesizingMethodParameter { + + @Nullable + private volatile Annotation[] combinedAnnotations; + + public AnnotatedMethodParameter(int index) { + super(AnnotatedMethod.this.getBridgedMethod(), index); + } + + protected AnnotatedMethodParameter(AnnotatedMethodParameter original) { + super(original); + this.combinedAnnotations = original.combinedAnnotations; + } + + @Override + @NonNull + public Method getMethod() { + return AnnotatedMethod.this.getBridgedMethod(); + } + + @Override + public Class getContainingClass() { + return AnnotatedMethod.this.getContainingClass(); + } + + @Override + @Nullable + public T getMethodAnnotation(Class annotationType) { + return AnnotatedMethod.this.getMethodAnnotation(annotationType); + } + + @Override + public boolean hasMethodAnnotation(Class annotationType) { + return AnnotatedMethod.this.hasMethodAnnotation(annotationType); + } + + @Override + public Annotation[] getParameterAnnotations() { + Annotation[] anns = this.combinedAnnotations; + if (anns == null) { + anns = super.getParameterAnnotations(); + int index = getParameterIndex(); + if (index >= 0) { + for (Annotation[][] ifcAnns : getInheritedParameterAnnotations()) { + if (index < ifcAnns.length) { + Annotation[] paramAnns = ifcAnns[index]; + if (paramAnns.length > 0) { + List merged = new ArrayList<>(anns.length + paramAnns.length); + merged.addAll(Arrays.asList(anns)); + for (Annotation paramAnn : paramAnns) { + boolean existingType = false; + for (Annotation ann : anns) { + if (ann.annotationType() == paramAnn.annotationType()) { + existingType = true; + break; + } + } + if (!existingType) { + merged.add(adaptAnnotation(paramAnn)); + } + } + anns = merged.toArray(new Annotation[0]); + } + } + } + } + this.combinedAnnotations = anns; + } + return anns; + } + + @Override + public AnnotatedMethodParameter clone() { + return new AnnotatedMethodParameter(this); + } + } + + + /** + * A MethodParameter for an AnnotatedMethod return type based on an actual return value. + */ + private class ReturnValueMethodParameter extends AnnotatedMethodParameter { + + @Nullable + private final Class returnValueType; + + public ReturnValueMethodParameter(@Nullable Object returnValue) { + super(-1); + this.returnValueType = (returnValue != null ? returnValue.getClass() : null); + } + + protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { + super(original); + this.returnValueType = original.returnValueType; + } + + @Override + public Class getParameterType() { + return (this.returnValueType != null ? this.returnValueType : super.getParameterType()); + } + + @Override + public ReturnValueMethodParameter clone() { + return new ReturnValueMethodParameter(this); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java index febfc54a6f1a..7732e008062e 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ public class AnnotationAttributes extends LinkedHashMap { final String displayName; - boolean validated = false; + final boolean validated; /** @@ -62,6 +62,7 @@ public class AnnotationAttributes extends LinkedHashMap { public AnnotationAttributes() { this.annotationType = null; this.displayName = UNKNOWN; + this.validated = false; } /** @@ -73,6 +74,7 @@ public AnnotationAttributes(int initialCapacity) { super(initialCapacity); this.annotationType = null; this.displayName = UNKNOWN; + this.validated = false; } /** @@ -85,6 +87,7 @@ public AnnotationAttributes(Map map) { super(map); this.annotationType = null; this.displayName = UNKNOWN; + this.validated = false; } /** @@ -108,9 +111,7 @@ public AnnotationAttributes(AnnotationAttributes other) { * @since 4.2 */ public AnnotationAttributes(Class annotationType) { - Assert.notNull(annotationType, "'annotationType' must not be null"); - this.annotationType = annotationType; - this.displayName = annotationType.getName(); + this(annotationType, false); } /** @@ -142,6 +143,7 @@ public AnnotationAttributes(String annotationType, @Nullable ClassLoader classLo Assert.notNull(annotationType, "'annotationType' must not be null"); this.annotationType = getAnnotationType(annotationType, classLoader); this.displayName = annotationType; + this.validated = false; } @SuppressWarnings("unchecked") @@ -327,8 +329,7 @@ public AnnotationAttributes[] getAnnotationArray(String attributeName) { */ @SuppressWarnings("unchecked") public A[] getAnnotationArray(String attributeName, Class annotationType) { - Object array = Array.newInstance(annotationType, 0); - return (A[]) getRequiredAttribute(attributeName, array.getClass()); + return (A[]) getRequiredAttribute(attributeName, annotationType.arrayType()); } /** @@ -350,39 +351,29 @@ public A[] getAnnotationArray(String attributeName, Class private T getRequiredAttribute(String attributeName, Class expectedType) { Assert.hasText(attributeName, "'attributeName' must not be null or empty"); Object value = get(attributeName); - assertAttributePresence(attributeName, value); - assertNotException(attributeName, value); - if (!expectedType.isInstance(value) && expectedType.isArray() && - expectedType.getComponentType().isInstance(value)) { - Object array = Array.newInstance(expectedType.getComponentType(), 1); - Array.set(array, 0, value); - value = array; + if (value == null) { + throw new IllegalArgumentException(String.format( + "Attribute '%s' not found in attributes for annotation [%s]", + attributeName, this.displayName)); } - assertAttributeType(attributeName, value, expectedType); - return (T) value; - } - - private void assertAttributePresence(String attributeName, Object attributeValue) { - Assert.notNull(attributeValue, () -> String.format( - "Attribute '%s' not found in attributes for annotation [%s]", - attributeName, this.displayName)); - } - - private void assertNotException(String attributeName, Object attributeValue) { - if (attributeValue instanceof Throwable throwable) { + if (value instanceof Throwable throwable) { throw new IllegalArgumentException(String.format( "Attribute '%s' for annotation [%s] was not resolvable due to exception [%s]", - attributeName, this.displayName, attributeValue), throwable); + attributeName, this.displayName, value), throwable); } - } - - private void assertAttributeType(String attributeName, Object attributeValue, Class expectedType) { - if (!expectedType.isInstance(attributeValue)) { + if (!expectedType.isInstance(value) && expectedType.isArray() && + expectedType.componentType().isInstance(value)) { + Object array = Array.newInstance(expectedType.componentType(), 1); + Array.set(array, 0, value); + value = array; + } + if (!expectedType.isInstance(value)) { throw new IllegalArgumentException(String.format( "Attribute '%s' is of type %s, but %s was expected in attributes for annotation [%s]", - attributeName, attributeValue.getClass().getSimpleName(), expectedType.getSimpleName(), + attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName(), this.displayName)); } + return (T) value; } @Override diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java index 1cddc5930667..3ac17cd27e56 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java @@ -92,7 +92,7 @@ public Integer getPriority(Object obj) { return OrderUtils.getPriority(clazz); } Integer priority = OrderUtils.getPriority(obj.getClass()); - if (priority == null && obj instanceof DecoratingProxy decoratingProxy) { + if (priority == null && obj instanceof DecoratingProxy decoratingProxy) { return getPriority(decoratingProxy.getDecoratedClass()); } return priority; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java index 831e5ce20e5b..e407b42fa8a1 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java @@ -218,7 +218,7 @@ private boolean isAliasPair(Method target) { } private boolean isCompatibleReturnType(Class attributeType, Class targetType) { - return (attributeType == targetType || attributeType == targetType.getComponentType()); + return (attributeType == targetType || attributeType == targetType.componentType()); } private void processAliases() { @@ -403,9 +403,9 @@ private boolean computeSynthesizableFlag(Set> visite for (int i = 0; i < attributeMethods.size(); i++) { Method method = attributeMethods.get(i); Class type = method.getReturnType(); - if (type.isAnnotation() || (type.isArray() && type.getComponentType().isAnnotation())) { + if (type.isAnnotation() || (type.isArray() && type.componentType().isAnnotation())) { Class annotationType = - (Class) (type.isAnnotation() ? type : type.getComponentType()); + (Class) (type.isAnnotation() ? type : type.componentType()); // Ensure we have not yet visited the current nested annotation type, in order // to avoid infinite recursion for JVM languages other than Java that support // recursive annotation definitions. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 47baaae305ec..506e286030de 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -650,11 +650,10 @@ public static Class findAnnotationDeclaringClassForTypes( return null; } - return (Class) MergedAnnotations.from(clazz, SearchStrategy.SUPERCLASS) - .stream() + MergedAnnotation merged = MergedAnnotations.from(clazz, SearchStrategy.SUPERCLASS).stream() .filter(MergedAnnotationPredicates.typeIn(annotationTypes).and(MergedAnnotation::isDirectlyPresent)) - .map(MergedAnnotation::getSource) .findFirst().orElse(null); + return (merged != null && merged.getSource() instanceof Class sourceClass ? sourceClass : null); } /** @@ -757,7 +756,7 @@ public static boolean isInJavaLangAnnotationPackage(@Nullable String annotationT * Google App Engine's late arrival of {@code TypeNotPresentExceptionProxy} for * {@code Class} values (instead of early {@code Class.getAnnotations() failure}). *

    This method not failing indicates that {@link #getAnnotationAttributes(Annotation)} - * won't failure either (when attempted later on). + * won't fail either (when attempted later on). * @param annotation the annotation to validate * @throws IllegalStateException if a declared {@code Class} attribute could not be read * @since 4.3.15 @@ -985,8 +984,12 @@ public static void postProcessAnnotationAttributes(@Nullable Object annotatedEle } } - private static Object getAttributeValueForMirrorResolution(Method attribute, Object attributes) { - Object result = ((AnnotationAttributes) attributes).get(attribute.getName()); + @Nullable + private static Object getAttributeValueForMirrorResolution(Method attribute, @Nullable Object attributes) { + if (!(attributes instanceof AnnotationAttributes annotationAttributes)) { + return null; + } + Object result = annotationAttributes.get(attribute.getName()); return (result instanceof DefaultValueHolder defaultValueHolder ? defaultValueHolder.defaultValue : result); } @@ -1011,7 +1014,7 @@ private static Object adaptValue( } if (value instanceof Annotation[] annotations) { Annotation[] synthesized = (Annotation[]) Array.newInstance( - annotations.getClass().getComponentType(), annotations.length); + annotations.getClass().componentType(), annotations.length); for (int i = 0; i < annotations.length; i++) { synthesized[i] = MergedAnnotation.from(annotatedElement, annotations[i]).synthesize(); } @@ -1049,17 +1052,16 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String return null; } try { - Method method = annotation.annotationType().getDeclaredMethod(attributeName); - return invokeAnnotationMethod(method, annotation); - } - catch (NoSuchMethodException ex) { - return null; + for (Method method : annotation.annotationType().getDeclaredMethods()) { + if (method.getName().equals(attributeName) && method.getParameterCount() == 0) { + return invokeAnnotationMethod(method, annotation); + } + } } catch (Throwable ex) { - rethrowAnnotationConfigurationException(ex); - handleIntrospectionFailure(annotation.getClass(), ex); - return null; + handleValueRetrievalFailure(annotation, ex); } + return null; } /** @@ -1073,14 +1075,18 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String * @return the value returned from the method invocation * @since 5.3.24 */ - static Object invokeAnnotationMethod(Method method, Object annotation) { + @Nullable + static Object invokeAnnotationMethod(Method method, @Nullable Object annotation) { + if (annotation == null) { + return null; + } if (Proxy.isProxyClass(annotation.getClass())) { try { InvocationHandler handler = Proxy.getInvocationHandler(annotation); return handler.invoke(annotation, method, null); } catch (Throwable ex) { - // ignore and fall back to reflection below + // Ignore and fall back to reflection below } } return ReflectionUtils.invokeMethod(method, annotation); @@ -1114,20 +1120,32 @@ static void rethrowAnnotationConfigurationException(Throwable ex) { * @see #rethrowAnnotationConfigurationException * @see IntrospectionFailureLogger */ - static void handleIntrospectionFailure(@Nullable AnnotatedElement element, Throwable ex) { + static void handleIntrospectionFailure(AnnotatedElement element, Throwable ex) { rethrowAnnotationConfigurationException(ex); IntrospectionFailureLogger logger = IntrospectionFailureLogger.INFO; boolean meta = false; if (element instanceof Class clazz && Annotation.class.isAssignableFrom(clazz)) { - // Meta-annotation or (default) value lookup on an annotation type + // Meta-annotation introspection failure logger = IntrospectionFailureLogger.DEBUG; meta = true; } if (logger.isEnabled()) { - String message = meta ? - "Failed to meta-introspect annotation " : - "Failed to introspect annotations on "; - logger.log(message + element + ": " + ex); + logger.log("Failed to " + (meta ? "meta-introspect annotation " : "introspect annotations on ") + + element + ": " + ex); + } + } + + /** + * Handle the supplied value retrieval exception. + * @param annotation the annotation instance from which to retrieve the value + * @param ex the exception that we encountered + * @see #handleIntrospectionFailure + */ + private static void handleValueRetrievalFailure(Annotation annotation, Throwable ex) { + rethrowAnnotationConfigurationException(ex); + IntrospectionFailureLogger logger = IntrospectionFailureLogger.INFO; + if (logger.isEnabled()) { + logger.log("Failed to retrieve value from " + annotation + ": " + ex); } } @@ -1292,7 +1310,7 @@ static Annotation[] synthesizeAnnotationArray(Annotation[] annotations, Annotate return annotations; } Annotation[] synthesized = (Annotation[]) Array.newInstance( - annotations.getClass().getComponentType(), annotations.length); + annotations.getClass().componentType(), annotations.length); for (int i = 0; i < annotations.length; i++) { synthesized[i] = synthesizeAnnotation(annotations[i], annotatedElement); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index 61fea0685226..5bd7168f96a1 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,7 +98,6 @@ private static R process(C context, AnnotatedElement source, } @Nullable - @SuppressWarnings("deprecation") private static R processClass(C context, Class source, SearchStrategy searchStrategy, Predicate> searchEnclosingClass, AnnotationsProcessor processor) { @@ -128,29 +127,31 @@ private static R processClassInheritedAnnotations(C context, Class sou if (result != null) { return result; } - Annotation[] declaredAnnotations = getDeclaredAnnotations(source, true); - if (relevant == null && declaredAnnotations.length > 0) { - relevant = root.getAnnotations(); - remaining = relevant.length; - } - for (int i = 0; i < declaredAnnotations.length; i++) { - if (declaredAnnotations[i] != null) { - boolean isRelevant = false; - for (int relevantIndex = 0; relevantIndex < relevant.length; relevantIndex++) { - if (relevant[relevantIndex] != null && - declaredAnnotations[i].annotationType() == relevant[relevantIndex].annotationType()) { - isRelevant = true; - relevant[relevantIndex] = null; - remaining--; - break; + Annotation[] declaredAnns = getDeclaredAnnotations(source, true); + if (declaredAnns.length > 0) { + if (relevant == null) { + relevant = root.getAnnotations(); + remaining = relevant.length; + } + for (int i = 0; i < declaredAnns.length; i++) { + if (declaredAnns[i] != null) { + boolean isRelevant = false; + for (int relevantIndex = 0; relevantIndex < relevant.length; relevantIndex++) { + if (relevant[relevantIndex] != null && + declaredAnns[i].annotationType() == relevant[relevantIndex].annotationType()) { + isRelevant = true; + relevant[relevantIndex] = null; + remaining--; + break; + } + } + if (!isRelevant) { + declaredAnns[i] = null; } - } - if (!isRelevant) { - declaredAnnotations[i] = null; } } } - result = processor.doWithAnnotations(context, aggregateIndex, source, declaredAnnotations); + result = processor.doWithAnnotations(context, aggregateIndex, source, declaredAnns); if (result != null) { return result; } @@ -237,7 +238,6 @@ private static R processClassHierarchy(C context, int[] aggregateIndex, C } @Nullable - @SuppressWarnings("deprecation") private static R processMethod(C context, Method source, SearchStrategy searchStrategy, AnnotationsProcessor processor) { @@ -336,11 +336,10 @@ private static Method[] getBaseTypeMethods(C context, Class baseType) { Method[] methods = baseTypeMethodsCache.get(baseType); if (methods == null) { - boolean isInterface = baseType.isInterface(); - methods = isInterface ? baseType.getMethods() : ReflectionUtils.getDeclaredMethods(baseType); + methods = ReflectionUtils.getDeclaredMethods(baseType); int cleared = 0; for (int i = 0; i < methods.length; i++) { - if ((!isInterface && Modifier.isPrivate(methods[i].getModifiers())) || + if (Modifier.isPrivate(methods[i].getModifiers()) || hasPlainJavaAnnotationsOnly(methods[i]) || getDeclaredAnnotations(methods[i], false).length == 0) { methods[i] = null; @@ -454,7 +453,7 @@ static Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defe for (int i = 0; i < annotations.length; i++) { Annotation annotation = annotations[i]; if (isIgnorable(annotation.annotationType()) || - !AttributeMethods.forAnnotationType(annotation.annotationType()).isValid(annotation)) { + !AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation)) { annotations[i] = null; } else { @@ -509,7 +508,6 @@ static boolean hasPlainJavaAnnotationsOnly(Class type) { return (type.getName().startsWith("java.") || type == Ordered.class); } - @SuppressWarnings("deprecation") private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy, Predicate> searchEnclosingClass) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index a828ebe44b5a..c24f51b6aba9 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ final class AttributeMethods { if (m1 != null && m2 != null) { return m1.getName().compareTo(m2.getName()); } - return m1 != null ? -1 : 1; + return (m1 != null ? -1 : 1); }; @@ -73,7 +73,7 @@ private AttributeMethods(@Nullable Class annotationType, M if (!foundDefaultValueMethod && (method.getDefaultValue() != null)) { foundDefaultValueMethod = true; } - if (!foundNestedAnnotation && (type.isAnnotation() || (type.isArray() && type.getComponentType().isAnnotation()))) { + if (!foundNestedAnnotation && (type.isAnnotation() || (type.isArray() && type.componentType().isAnnotation()))) { foundNestedAnnotation = true; } ReflectionUtils.makeAccessible(method); @@ -87,18 +87,26 @@ private AttributeMethods(@Nullable Class annotationType, M /** * Determine if values from the given annotation can be safely accessed without * causing any {@link TypeNotPresentException TypeNotPresentExceptions}. + *

    This method is designed to cover Google App Engine's late arrival of such + * exceptions for {@code Class} values (instead of the more typical early + * {@code Class.getAnnotations() failure} on a regular JVM). * @param annotation the annotation to check * @return {@code true} if all values are present * @see #validate(Annotation) */ - boolean isValid(Annotation annotation) { + boolean canLoad(Annotation annotation) { assertAnnotation(annotation); for (int i = 0; i < size(); i++) { if (canThrowTypeNotPresentException(i)) { try { AnnotationUtils.invokeAnnotationMethod(get(i), annotation); } + catch (IllegalStateException ex) { + // Plain invocation failure to expose -> leave up to attribute retrieval + // (if any) where such invocation failure will be logged eventually. + } catch (Throwable ex) { + // TypeNotPresentException etc. -> annotation type not actually loadable. return false; } } @@ -108,13 +116,13 @@ boolean isValid(Annotation annotation) { /** * Check if values from the given annotation can be safely accessed without causing - * any {@link TypeNotPresentException TypeNotPresentExceptions}. In particular, - * this method is designed to cover Google App Engine's late arrival of such + * any {@link TypeNotPresentException TypeNotPresentExceptions}. + *

    This method is designed to cover Google App Engine's late arrival of such * exceptions for {@code Class} values (instead of the more typical early - * {@code Class.getAnnotations() failure}). + * {@code Class.getAnnotations() failure} on a regular JVM). * @param annotation the annotation to validate * @throws IllegalStateException if a declared {@code Class} attribute could not be read - * @see #isValid(Annotation) + * @see #canLoad(Annotation) */ void validate(Annotation annotation) { assertAnnotation(annotation); @@ -123,6 +131,9 @@ void validate(Annotation annotation) { try { AnnotationUtils.invokeAnnotationMethod(get(i), annotation); } + catch (IllegalStateException ex) { + throw ex; + } catch (Throwable ex) { throw new IllegalStateException("Could not obtain annotation attribute value for " + get(i).getName() + " declared on " + annotation.annotationType(), ex); @@ -147,7 +158,7 @@ private void assertAnnotation(Annotation annotation) { @Nullable Method get(String name) { int index = indexOf(name); - return index != -1 ? this.attributeMethods[index] : null; + return (index != -1 ? this.attributeMethods[index] : null); } /** diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java index 5a26e0c84aca..7d054454a1c8 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -507,7 +507,7 @@ static Search search(SearchStrategy searchStrategy) { * * @since 6.0 */ - static final class Search { + final class Search { static final Predicate> always = clazz -> true; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java index 0d2e8372d078..01c0bafd1b21 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -167,19 +167,21 @@ private MergedAnnotation find(Object requiredType, MergedAnnotation result = null; for (int i = 0; i < this.annotations.length; i++) { MergedAnnotation root = this.annotations[i]; - AnnotationTypeMappings mappings = this.mappings[i]; - for (int mappingIndex = 0; mappingIndex < mappings.size(); mappingIndex++) { - AnnotationTypeMapping mapping = mappings.get(mappingIndex); - if (!isMappingForType(mapping, requiredType)) { - continue; - } - MergedAnnotation candidate = (mappingIndex == 0 ? (MergedAnnotation) root : - TypeMappedAnnotation.createIfPossible(mapping, root, IntrospectionFailureLogger.INFO)); - if (candidate != null && (predicate == null || predicate.test(candidate))) { - if (selector.isBestCandidate(candidate)) { - return candidate; + if (root != null) { + AnnotationTypeMappings mappings = this.mappings[i]; + for (int mappingIndex = 0; mappingIndex < mappings.size(); mappingIndex++) { + AnnotationTypeMapping mapping = mappings.get(mappingIndex); + if (!isMappingForType(mapping, requiredType)) { + continue; + } + MergedAnnotation candidate = (mappingIndex == 0 ? (MergedAnnotation) root : + TypeMappedAnnotation.createIfPossible(mapping, root, IntrospectionFailureLogger.INFO)); + if (candidate != null && (predicate == null || predicate.test(candidate))) { + if (selector.isBestCandidate(candidate)) { + return candidate; + } + result = (result != null ? selector.select(result, candidate) : candidate); } - result = (result != null ? selector.select(result, candidate) : candidate); } } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java index 19fbc577b885..a18253f48cae 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ public abstract class OrderUtils { /** Cache marker for a non-annotated Class. */ private static final Object NOT_ANNOTATED = new Object(); - private static final String JAVAX_PRIORITY_ANNOTATION = "jakarta.annotation.Priority"; + private static final String JAKARTA_PRIORITY_ANNOTATION = "jakarta.annotation.Priority"; /** Cache for @Order value (or NOT_ANNOTATED marker) per Class. */ static final Map orderCache = new ConcurrentReferenceHashMap<>(64); @@ -124,7 +124,7 @@ private static Integer findOrder(MergedAnnotations annotations) { if (orderAnnotation.isPresent()) { return orderAnnotation.getInt(MergedAnnotation.VALUE); } - MergedAnnotation priorityAnnotation = annotations.get(JAVAX_PRIORITY_ANNOTATION); + MergedAnnotation priorityAnnotation = annotations.get(JAKARTA_PRIORITY_ANNOTATION); if (priorityAnnotation.isPresent()) { return priorityAnnotation.getInt(MergedAnnotation.VALUE); } @@ -139,7 +139,7 @@ private static Integer findOrder(MergedAnnotations annotations) { */ @Nullable public static Integer getPriority(Class type) { - return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAVAX_PRIORITY_ANNOTATION) + return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAKARTA_PRIORITY_ANNOTATION) .getValue(MergedAnnotation.VALUE, Integer.class).orElse(null); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java index adc33e3aa2ef..581ffeb9c8b8 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -20,6 +20,7 @@ import java.lang.annotation.Repeatable; import java.lang.reflect.Method; import java.util.Map; +import java.util.Objects; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -91,7 +92,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.parent); + return Objects.hashCode(this.parent); } @@ -174,7 +175,7 @@ private static Object computeRepeatedAnnotationsMethod(Class returnType = method.getReturnType(); if (returnType.isArray()) { - Class componentType = returnType.getComponentType(); + Class componentType = returnType.componentType(); if (Annotation.class.isAssignableFrom(componentType) && componentType.isAnnotationPresent(Repeatable.class)) { return method; @@ -211,7 +212,7 @@ private static class ExplicitRepeatableContainer extends RepeatableContainers { throw new NoSuchMethodException("No value method found"); } Class returnType = valueMethod.getReturnType(); - if (!returnType.isArray() || returnType.getComponentType() != repeatable) { + if (!returnType.isArray() || returnType.componentType() != repeatable) { throw new AnnotationConfigurationException( "Container type [%s] must declare a 'value' attribute for an array of type [%s]" .formatted(container.getName(), repeatable.getName())); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java index 69847101d59e..32a75d7286c5 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.util.Arrays; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; @@ -74,29 +73,23 @@ private SynthesizedMergedAnnotationInvocationHandler(MergedAnnotation annotat @Override public Object invoke(Object proxy, Method method, Object[] args) { - if (ReflectionUtils.isEqualsMethod(method)) { - return annotationEquals(args[0]); - } - if (ReflectionUtils.isHashCodeMethod(method)) { - return annotationHashCode(); - } - if (ReflectionUtils.isToStringMethod(method)) { - return annotationToString(); - } - if (isAnnotationTypeMethod(method)) { - return this.type; - } if (this.attributes.indexOf(method.getName()) != -1) { return getAttributeValue(method); } + if (method.getParameterCount() == 0) { + switch (method.getName()) { + case "annotationType": return this.type; + case "hashCode": return annotationHashCode(); + case "toString": return annotationToString(); + } + } + if (ReflectionUtils.isEqualsMethod(method)) { + return annotationEquals(args[0]); + } throw new AnnotationConfigurationException(String.format( "Method [%s] is unsupported for synthesized annotation type [%s]", method, this.type)); } - private boolean isAnnotationTypeMethod(Method method) { - return (method.getName().equals("annotationType") && method.getParameterCount() == 0); - } - /** * See {@link Annotation#equals(Object)} for a definition of the required algorithm. * @param other the other object to compare against @@ -136,44 +129,11 @@ private Integer computeHashCode() { for (int i = 0; i < this.attributes.size(); i++) { Method attribute = this.attributes.get(i); Object value = getAttributeValue(attribute); - hashCode += (127 * attribute.getName().hashCode()) ^ getValueHashCode(value); + hashCode += (127 * attribute.getName().hashCode()) ^ ObjectUtils.nullSafeHashCode(value); } return hashCode; } - private int getValueHashCode(Object value) { - // Use Arrays.hashCode(...) since Spring's ObjectUtils doesn't comply - // with the requirements specified in Annotation#hashCode(). - if (value instanceof boolean[] booleans) { - return Arrays.hashCode(booleans); - } - if (value instanceof byte[] bytes) { - return Arrays.hashCode(bytes); - } - if (value instanceof char[] chars) { - return Arrays.hashCode(chars); - } - if (value instanceof double[] doubles) { - return Arrays.hashCode(doubles); - } - if (value instanceof float[] floats) { - return Arrays.hashCode(floats); - } - if (value instanceof int[] ints) { - return Arrays.hashCode(ints); - } - if (value instanceof long[] longs) { - return Arrays.hashCode(longs); - } - if (value instanceof short[] shorts) { - return Arrays.hashCode(shorts); - } - if (value instanceof Object[] objects) { - return Arrays.hashCode(objects); - } - return value.hashCode(); - } - private String annotationToString() { String string = this.string; if (string == null) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java index 27ced8751cba..1d0d093aa2c1 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -228,7 +228,7 @@ public MergedAnnotation[] getAnnotationArray( int attributeIndex = getAttributeIndex(attributeName, true); Method attribute = this.mapping.getAttributes().get(attributeIndex); - Class componentType = attribute.getReturnType().getComponentType(); + Class componentType = attribute.getReturnType().componentType(); Assert.notNull(type, "Type must not be null"); Assert.notNull(componentType, () -> "Attribute " + attributeName + " is not an array"); Assert.isAssignable(type, componentType, () -> "Attribute " + attributeName + " component type mismatch:"); @@ -286,7 +286,7 @@ public > T asMap(Function, T> private Class getTypeForMapOptions(Method attribute, Adapt[] adaptations) { Class attributeType = attribute.getReturnType(); - Class componentType = (attributeType.isArray() ? attributeType.getComponentType() : attributeType); + Class componentType = (attributeType.isArray() ? attributeType.componentType() : attributeType); if (Adapt.CLASS_TO_STRING.isIn(adaptations) && componentType == Class.class) { return (attributeType.isArray() ? String[].class : String.class); } @@ -309,7 +309,7 @@ private > Object adaptValueForMapOptions(Method at return result; } Object result = Array.newInstance( - attribute.getReturnType().getComponentType(), annotations.length); + attribute.getReturnType().componentType(), annotations.length); for (int i = 0; i < annotations.length; i++) { Array.set(result, i, annotations[i].synthesize()); } @@ -322,12 +322,13 @@ private > Object adaptValueForMapOptions(Method at @SuppressWarnings("unchecked") protected A createSynthesizedAnnotation() { // Check root annotation - if (isTargetAnnotation(this.rootAttributes) && !isSynthesizable((Annotation) this.rootAttributes)) { - return (A) this.rootAttributes; + if (this.rootAttributes instanceof Annotation ann && isTargetAnnotation(ann) && !isSynthesizable(ann)) { + return (A) ann; } // Check meta-annotation - else if (isTargetAnnotation(this.mapping.getAnnotation()) && !isSynthesizable(this.mapping.getAnnotation())) { - return (A) this.mapping.getAnnotation(); + Annotation meta = this.mapping.getAnnotation(); + if (meta != null && isTargetAnnotation(meta) && !isSynthesizable(meta)) { + return (A) meta; } return SynthesizedMergedAnnotationInvocationHandler.createProxy(this, getType()); } @@ -338,7 +339,7 @@ else if (isTargetAnnotation(this.mapping.getAnnotation()) && !isSynthesizable(th * @param obj the object to check * @since 5.3.22 */ - private boolean isTargetAnnotation(@Nullable Object obj) { + private boolean isTargetAnnotation(Object obj) { return getType().isInstance(obj); } @@ -432,7 +433,7 @@ private Object getValueFromMetaAnnotation(int attributeIndex, boolean forMirrorR } @Nullable - private Object getValueForMirrorResolution(Method attribute, Object annotation) { + private Object getValueForMirrorResolution(Method attribute, @Nullable Object annotation) { int attributeIndex = this.mapping.getAttributes().indexOf(attribute); boolean valueAttribute = VALUE.equals(attribute.getName()); return getValue(attributeIndex, !valueAttribute, true); @@ -470,8 +471,8 @@ else if (value instanceof MergedAnnotation annotation && type.isAnnotation()) value = annotation.synthesize(); } else if (value instanceof MergedAnnotation[] annotations && - type.isArray() && type.getComponentType().isAnnotation()) { - Object array = Array.newInstance(type.getComponentType(), annotations.length); + type.isArray() && type.componentType().isAnnotation()) { + Object array = Array.newInstance(type.componentType(), annotations.length); for (int i = 0; i < annotations.length; i++) { Array.set(array, i, annotations[i].synthesize()); } @@ -495,11 +496,11 @@ private Object adaptForAttribute(Method attribute, Object value) { if (attributeType.isAnnotation()) { return adaptToMergedAnnotation(value, (Class) attributeType); } - if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) { + if (attributeType.isArray() && attributeType.componentType().isAnnotation()) { MergedAnnotation[] result = new MergedAnnotation[Array.getLength(value)]; for (int i = 0; i < result.length; i++) { result[i] = adaptToMergedAnnotation(Array.get(value, i), - (Class) attributeType.getComponentType()); + (Class) attributeType.componentType()); } return result; } @@ -510,7 +511,7 @@ private Object adaptForAttribute(Method attribute, Object value) { return value; } if (attributeType.isArray() && isEmptyObjectArray(value)) { - return emptyArray(attributeType.getComponentType()); + return emptyArray(attributeType.componentType()); } if (!attributeType.isInstance(value)) { throw new IllegalStateException("Attribute '" + attribute.getName() + @@ -561,7 +562,7 @@ private Class getAdaptType(Method attribute, Class type) { if (attributeType.isAnnotation()) { return (Class) MergedAnnotation.class; } - if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) { + if (attributeType.isArray() && attributeType.componentType().isAnnotation()) { return (Class) MergedAnnotation[].class; } return (Class) ClassUtils.resolvePrimitiveIfNecessary(attributeType); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java index 581a74c10bd6..7187cceb42ae 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -418,7 +418,10 @@ private MergedAnnotation process( Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); if (repeatedAnnotations != null) { - return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + MergedAnnotation result = doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + if (result != null) { + return result; + } } AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( annotation.annotationType(), repeatableContainers, annotationFilter); diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractCharSequenceDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractCharSequenceDecoder.java new file mode 100644 index 000000000000..340792259bbd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractCharSequenceDecoder.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.codec; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.LimitedDataBufferList; +import org.springframework.core.log.LogFormatUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Abstract base class that decodes from a data buffer stream to a + * {@code CharSequence} stream. + * + * @author Arjen Poutsma + * @since 6.1 + * @param the character sequence type + */ +public abstract class AbstractCharSequenceDecoder extends AbstractDataBufferDecoder { + + /** The default charset to use, i.e. "UTF-8". */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** The default delimiter strings to use, i.e. {@code \r\n} and {@code \n}. */ + public static final List DEFAULT_DELIMITERS = List.of("\r\n", "\n"); + + + private final List delimiters; + + private final boolean stripDelimiter; + + private Charset defaultCharset = DEFAULT_CHARSET; + + private final ConcurrentMap delimitersCache = new ConcurrentHashMap<>(); + + + /** + * Create a new {@code AbstractCharSequenceDecoder} with the given parameters. + */ + protected AbstractCharSequenceDecoder(List delimiters, boolean stripDelimiter, MimeType... mimeTypes) { + super(mimeTypes); + Assert.notEmpty(delimiters, "'delimiters' must not be empty"); + this.delimiters = new ArrayList<>(delimiters); + this.stripDelimiter = stripDelimiter; + } + + + /** + * Set the default character set to fall back on if the MimeType does not specify any. + *

    By default this is {@code UTF-8}. + * @param defaultCharset the charset to fall back on + */ + public void setDefaultCharset(Charset defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * Return the configured {@link #setDefaultCharset(Charset) defaultCharset}. + */ + public Charset getDefaultCharset() { + return this.defaultCharset; + } + + + @Override + public final Flux decode(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + byte[][] delimiterBytes = getDelimiterBytes(mimeType); + + LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize()); + DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes); + + return Flux.from(input) + .concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks)) + .concatWith(Mono.defer(() -> { + if (chunks.isEmpty()) { + return Mono.empty(); + } + DataBuffer lastBuffer = chunks.get(0).factory().join(chunks); + chunks.clear(); + return Mono.just(lastBuffer); + })) + .doFinally(signalType -> chunks.releaseAndClear()) + .doOnDiscard(DataBuffer.class, DataBufferUtils::release) + .map(buffer -> decode(buffer, elementType, mimeType, hints)); + } + + private byte[][] getDelimiterBytes(@Nullable MimeType mimeType) { + return this.delimitersCache.computeIfAbsent(getCharset(mimeType), charset -> { + byte[][] result = new byte[this.delimiters.size()][]; + for (int i = 0; i < this.delimiters.size(); i++) { + result[i] = this.delimiters.get(i).getBytes(charset); + } + return result; + }); + } + + private Collection processDataBuffer(DataBuffer buffer, DataBufferUtils.Matcher matcher, + LimitedDataBufferList chunks) { + + boolean release = true; + try { + List result = null; + do { + int endIndex = matcher.match(buffer); + if (endIndex == -1) { + chunks.add(buffer); + release = false; + break; + } + DataBuffer split = buffer.split(endIndex + 1); + if (result == null) { + result = new ArrayList<>(); + } + int delimiterLength = matcher.delimiter().length; + if (chunks.isEmpty()) { + if (this.stripDelimiter) { + split.writePosition(split.writePosition() - delimiterLength); + } + result.add(split); + } + else { + chunks.add(split); + DataBuffer joined = buffer.factory().join(chunks); + if (this.stripDelimiter) { + joined.writePosition(joined.writePosition() - delimiterLength); + } + result.add(joined); + chunks.clear(); + } + } + while (buffer.readableByteCount() > 0); + return (result != null ? result : Collections.emptyList()); + } + finally { + if (release) { + DataBufferUtils.release(buffer); + } + } + } + + @Override + public final T decode(DataBuffer dataBuffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Charset charset = getCharset(mimeType); + T value = decodeInternal(dataBuffer, charset); + DataBufferUtils.release(dataBuffer); + LogFormatUtils.traceDebug(logger, traceOn -> { + String formatted = LogFormatUtils.formatValue(value, !traceOn); + return Hints.getLogPrefix(hints) + "Decoded " + formatted; + }); + return value; + } + + private Charset getCharset(@Nullable MimeType mimeType) { + if (mimeType != null) { + Charset charset = mimeType.getCharset(); + if (charset != null) { + return charset; + } + } + return getDefaultCharset(); + } + + + /** + * Template method that decodes the given data buffer into {@code T}, given + * the charset. + */ + protected abstract T decodeInternal(DataBuffer dataBuffer, Charset charset); + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java index 0fd5870b2a2e..9bcb6430f02d 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,6 @@ * @since 5.0 * @param the element type */ -@SuppressWarnings("deprecation") public abstract class AbstractDataBufferDecoder extends AbstractDecoder { private int maxInMemorySize = 256 * 1024; diff --git a/spring-core/src/main/java/org/springframework/core/codec/CharBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/CharBufferDecoder.java new file mode 100644 index 000000000000..6745a2e00c61 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/CharBufferDecoder.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.List; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decode from a data buffer stream to a {@code CharBuffer} stream, either splitting + * or aggregating incoming data chunks to realign along newlines delimiters + * and produce a stream of char buffers. This is useful for streaming but is also + * necessary to ensure that multi-byte characters can be decoded correctly, + * avoiding split-character issues. The default delimiters used by default are + * {@code \n} and {@code \r\n} but that can be customized. + * + * @author Markus Heiden + * @author Arjen Poutsma + * @since 6.1 + * @see CharSequenceEncoder + */ +public final class CharBufferDecoder extends AbstractCharSequenceDecoder { + + public CharBufferDecoder(List delimiters, boolean stripDelimiter, MimeType... mimeTypes) { + super(delimiters, stripDelimiter, mimeTypes); + } + + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return (elementType.resolve() == CharBuffer.class) && super.canDecode(elementType, mimeType); + } + + @Override + protected CharBuffer decodeInternal(DataBuffer dataBuffer, Charset charset) { + ByteBuffer byteBuffer = ByteBuffer.allocate(dataBuffer.readableByteCount()); + dataBuffer.toByteBuffer(byteBuffer); + return charset.decode(byteBuffer); + } + + + /** + * Create a {@code CharBufferDecoder} for {@code "text/plain"}. + */ + public static CharBufferDecoder textPlainOnly() { + return textPlainOnly(DEFAULT_DELIMITERS, true); + } + + /** + * Create a {@code CharBufferDecoder} for {@code "text/plain"}. + * @param delimiters delimiter strings to use to split the input stream + * @param stripDelimiter whether to remove delimiters from the resulting input strings + */ + public static CharBufferDecoder textPlainOnly(List delimiters, boolean stripDelimiter) { + var textPlain = new MimeType("text", "plain", DEFAULT_CHARSET); + return new CharBufferDecoder(delimiters, stripDelimiter, textPlain); + } + + /** + * Create a {@code CharBufferDecoder} that supports all MIME types. + */ + public static CharBufferDecoder allMimeTypes() { + return allMimeTypes(DEFAULT_DELIMITERS, true); + } + + /** + * Create a {@code CharBufferDecoder} that supports all MIME types. + * @param delimiters delimiter strings to use to split the input stream + * @param stripDelimiter whether to remove delimiters from the resulting input strings + */ + public static CharBufferDecoder allMimeTypes(List delimiters, boolean stripDelimiter) { + var textPlain = new MimeType("text", "plain", DEFAULT_CHARSET); + return new CharBufferDecoder(delimiters, stripDelimiter, textPlain, MimeTypeUtils.ALL); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/CodecException.java b/spring-core/src/main/java/org/springframework/core/codec/CodecException.java index 0cdd1de57dc2..f621858783a7 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/CodecException.java +++ b/spring-core/src/main/java/org/springframework/core/codec/CodecException.java @@ -34,7 +34,7 @@ public class CodecException extends NestedRuntimeException { * Create a new CodecException. * @param msg the detail message */ - public CodecException(String msg) { + public CodecException(@Nullable String msg) { super(msg); } @@ -43,7 +43,7 @@ public CodecException(String msg) { * @param msg the detail message * @param cause root cause for the exception, if any */ - public CodecException(String msg, @Nullable Throwable cause) { + public CodecException(@Nullable String msg, @Nullable Throwable cause) { super(msg, cause); } diff --git a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java index d37244b764d5..f49063f101ae 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,20 +95,19 @@ default T decode(DataBuffer buffer, ResolvableType targetType, @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { CompletableFuture future = decodeToMono(Mono.just(buffer), targetType, mimeType, hints).toFuture(); - Assert.state(future.isDone(), "DataBuffer decoding should have completed."); + Assert.state(future.isDone(), "DataBuffer decoding should have completed"); - Throwable failure; try { return future.get(); } catch (ExecutionException ex) { - failure = ex.getCause(); + Throwable cause = ex.getCause(); + throw (cause instanceof CodecException codecException ? codecException : + new DecodingException("Failed to decode: " + (cause != null ? cause.getMessage() : ex), cause)); } catch (InterruptedException ex) { - failure = ex; + throw new DecodingException("Interrupted during decode", ex); } - throw (failure instanceof CodecException codecException ? codecException : - new DecodingException("Failed to decode: " + failure.getMessage(), failure)); } /** diff --git a/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java b/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java index c825f08e8a38..873996dbcb36 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java +++ b/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java @@ -39,7 +39,7 @@ public class DecodingException extends CodecException { * Create a new DecodingException. * @param msg the detail message */ - public DecodingException(String msg) { + public DecodingException(@Nullable String msg) { super(msg); } @@ -48,7 +48,7 @@ public DecodingException(String msg) { * @param msg the detail message * @param cause root cause for the exception, if any */ - public DecodingException(String msg, @Nullable Throwable cause) { + public DecodingException(@Nullable String msg, @Nullable Throwable cause) { super(msg, cause); } diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java index 87f7ac478ae4..f9d92f5e6e52 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,10 +76,11 @@ public Resource decode(DataBuffer dataBuffer, ResolvableType elementType, } Class clazz = elementType.toClass(); - String filename = hints != null ? (String) hints.get(FILENAME_HINT) : null; + String filename = (hints != null ? (String) hints.get(FILENAME_HINT) : null); if (clazz == InputStreamResource.class) { return new InputStreamResource(new ByteArrayInputStream(bytes)) { @Override + @Nullable public String getFilename() { return filename; } @@ -92,6 +93,7 @@ public long contentLength() { else if (Resource.class.isAssignableFrom(clazz)) { return new ByteArrayResource(bytes) { @Override + @Nullable public String getFilename() { return filename; } diff --git a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java index 602734f43b48..d68abc2a33e3 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -17,26 +17,11 @@ package org.springframework.core.codec; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.LimitedDataBufferList; -import org.springframework.core.log.LogFormatUtils; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -55,48 +40,10 @@ * @since 5.0 * @see CharSequenceEncoder */ -public final class StringDecoder extends AbstractDataBufferDecoder { - - /** The default charset to use, i.e. "UTF-8". */ - public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - /** The default delimiter strings to use, i.e. {@code \r\n} and {@code \n}. */ - public static final List DEFAULT_DELIMITERS = List.of("\r\n", "\n"); - - - private final List delimiters; - - private final boolean stripDelimiter; - - private Charset defaultCharset = DEFAULT_CHARSET; - - private final ConcurrentMap delimitersCache = new ConcurrentHashMap<>(); - +public final class StringDecoder extends AbstractCharSequenceDecoder { private StringDecoder(List delimiters, boolean stripDelimiter, MimeType... mimeTypes) { - super(mimeTypes); - Assert.notEmpty(delimiters, "'delimiters' must not be empty"); - this.delimiters = new ArrayList<>(delimiters); - this.stripDelimiter = stripDelimiter; - } - - - /** - * Set the default character set to fall back on if the MimeType does not specify any. - *

    By default this is {@code UTF-8}. - * @param defaultCharset the charset to fall back on - * @since 5.2.9 - */ - public void setDefaultCharset(Charset defaultCharset) { - this.defaultCharset = defaultCharset; - } - - /** - * Return the configured {@link #setDefaultCharset(Charset) defaultCharset}. - * @since 5.2.9 - */ - public Charset getDefaultCharset() { - return this.defaultCharset; + super(delimiters, stripDelimiter, mimeTypes); } @@ -105,106 +52,12 @@ public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType return (elementType.resolve() == String.class && super.canDecode(elementType, mimeType)); } - @Override - public Flux decode(Publisher input, ResolvableType elementType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - byte[][] delimiterBytes = getDelimiterBytes(mimeType); - - LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize()); - DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes); - - return Flux.from(input) - .concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks)) - .concatWith(Mono.defer(() -> { - if (chunks.isEmpty()) { - return Mono.empty(); - } - DataBuffer lastBuffer = chunks.get(0).factory().join(chunks); - chunks.clear(); - return Mono.just(lastBuffer); - })) - .doFinally(signalType -> chunks.releaseAndClear()) - .doOnDiscard(DataBuffer.class, DataBufferUtils::release) - .map(buffer -> decode(buffer, elementType, mimeType, hints)); - } - - private byte[][] getDelimiterBytes(@Nullable MimeType mimeType) { - return this.delimitersCache.computeIfAbsent(getCharset(mimeType), charset -> { - byte[][] result = new byte[this.delimiters.size()][]; - for (int i = 0; i < this.delimiters.size(); i++) { - result[i] = this.delimiters.get(i).getBytes(charset); - } - return result; - }); - } - - private Collection processDataBuffer( - DataBuffer buffer, DataBufferUtils.Matcher matcher, LimitedDataBufferList chunks) { - - boolean release = true; - try { - List result = null; - do { - int endIndex = matcher.match(buffer); - if (endIndex == -1) { - chunks.add(buffer); - release = false; - break; - } - DataBuffer split = buffer.split(endIndex + 1); - if (result == null) { - result = new ArrayList<>(); - } - int delimiterLength = matcher.delimiter().length; - if (chunks.isEmpty()) { - if (this.stripDelimiter) { - split.writePosition(split.writePosition() - delimiterLength); - } - result.add(split); - } - else { - chunks.add(split); - DataBuffer joined = buffer.factory().join(chunks); - if (this.stripDelimiter) { - joined.writePosition(joined.writePosition() - delimiterLength); - } - result.add(joined); - chunks.clear(); - } - } - while (buffer.readableByteCount() > 0); - return (result != null ? result : Collections.emptyList()); - } - finally { - if (release) { - DataBufferUtils.release(buffer); - } - } - } @Override - public String decode(DataBuffer dataBuffer, ResolvableType elementType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - Charset charset = getCharset(mimeType); - String value = dataBuffer.toString(charset); - DataBufferUtils.release(dataBuffer); - LogFormatUtils.traceDebug(logger, traceOn -> { - String formatted = LogFormatUtils.formatValue(value, !traceOn); - return Hints.getLogPrefix(hints) + "Decoded " + formatted; - }); - return value; + protected String decodeInternal(DataBuffer dataBuffer, Charset charset) { + return dataBuffer.toString(charset); } - private Charset getCharset(@Nullable MimeType mimeType) { - if (mimeType != null && mimeType.getCharset() != null) { - return mimeType.getCharset(); - } - else { - return getDefaultCharset(); - } - } /** * Create a {@code StringDecoder} for {@code "text/plain"}. diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java index 92b33cd9149d..9c02d51cbcf4 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,6 +75,23 @@ public interface ConversionService { @Nullable T convert(@Nullable Object source, Class targetType); + /** + * Convert the given {@code source} to the specified {@code targetType}. + *

    Delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} + * and encapsulates the construction of the source type descriptor using + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param targetType the target type + * @return the converted value + * @throws ConversionException if a conversion exception occurred + * @throws IllegalArgumentException if targetType is {@code null} + * @since 6.1 + */ + @Nullable + default Object convert(@Nullable Object source, TypeDescriptor targetType) { + return convert(source, TypeDescriptor.forObject(source), targetType); + } + /** * Convert the given {@code source} to the specified {@code targetType}. * The TypeDescriptors provide additional context about the source and target locations diff --git a/spring-core/src/main/java/org/springframework/core/convert/Property.java b/spring-core/src/main/java/org/springframework/core/convert/Property.java index 1e774106cd2d..e0b601cdd46c 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/Property.java +++ b/spring-core/src/main/java/org/springframework/core/convert/Property.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; @@ -269,7 +270,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return (ObjectUtils.nullSafeHashCode(this.objectType) * 31 + ObjectUtils.nullSafeHashCode(this.name)); + return Objects.hash(this.objectType, this.name); } } diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index f02d0ea0a2b9..a61e62322147 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,8 +52,6 @@ @SuppressWarnings("serial") public class TypeDescriptor implements Serializable { - private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; - private static final Map, TypeDescriptor> commonTypesCache = new HashMap<>(32); private static final Class[] CACHED_COMMON_TYPES = { @@ -84,7 +82,7 @@ public class TypeDescriptor implements Serializable { public TypeDescriptor(MethodParameter methodParameter) { this.resolvableType = ResolvableType.forMethodParameter(methodParameter); this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType()); - this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ? + this.annotatedElement = AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ? methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations()); } @@ -96,7 +94,7 @@ public TypeDescriptor(MethodParameter methodParameter) { public TypeDescriptor(Field field) { this.resolvableType = ResolvableType.forField(field); this.type = this.resolvableType.resolve(field.getType()); - this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(field.getAnnotations()); } /** @@ -109,7 +107,7 @@ public TypeDescriptor(Property property) { Assert.notNull(property, "Property must not be null"); this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); this.type = this.resolvableType.resolve(property.getType()); - this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(property.getAnnotations()); } /** @@ -125,7 +123,7 @@ public TypeDescriptor(Property property) { public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { this.resolvableType = resolvableType; this.type = (type != null ? type : resolvableType.toClass()); - this.annotatedElement = new AnnotatedElementAdapter(annotations); + this.annotatedElement = AnnotatedElementAdapter.from(annotations); } @@ -170,6 +168,33 @@ public Object getSource() { return this.resolvableType.getSource(); } + + /** + * Create a type descriptor for a nested type declared within this descriptor. + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the property + * @return the nested type descriptor at the specified nesting level, or + * {@code null} if it could not be obtained + * @since 6.1 + */ + @Nullable + public TypeDescriptor nested(int nestingLevel) { + ResolvableType nested = this.resolvableType; + for (int i = 0; i < nestingLevel; i++) { + if (Object.class == nested.getType()) { + // Could be a collection type but we don't know about its element type, + // so let's just assume there is an element type of type Object... + } + else { + nested = nested.getNested(2); + } + } + if (nested == ResolvableType.NONE) { + return null; + } + return getRelatedIfResolvable(nested); + } + /** * Narrows this {@link TypeDescriptor} by setting its type to the class of the * provided value. @@ -335,9 +360,9 @@ public TypeDescriptor getElementTypeDescriptor() { return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations()); } if (Stream.class.isAssignableFrom(getType())) { - return getRelatedIfResolvable(this, getResolvableType().as(Stream.class).getGeneric(0)); + return getRelatedIfResolvable(getResolvableType().as(Stream.class).getGeneric(0)); } - return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0)); + return getRelatedIfResolvable(getResolvableType().asCollection().getGeneric(0)); } /** @@ -380,7 +405,7 @@ public boolean isMap() { @Nullable public TypeDescriptor getMapKeyTypeDescriptor() { Assert.state(isMap(), "Not a [java.util.Map]"); - return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(0)); + return getRelatedIfResolvable(getResolvableType().asMap().getGeneric(0)); } /** @@ -417,7 +442,7 @@ public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) { @Nullable public TypeDescriptor getMapValueTypeDescriptor() { Assert.state(isMap(), "Not a [java.util.Map]"); - return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1)); + return getRelatedIfResolvable(getResolvableType().asMap().getGeneric(1)); } /** @@ -438,10 +463,18 @@ public TypeDescriptor getMapValueTypeDescriptor() { * @see #narrow(Object) */ @Nullable - public TypeDescriptor getMapValueTypeDescriptor(Object mapValue) { + public TypeDescriptor getMapValueTypeDescriptor(@Nullable Object mapValue) { return narrow(mapValue, getMapValueTypeDescriptor()); } + @Nullable + private TypeDescriptor getRelatedIfResolvable(ResolvableType type) { + if (type.resolve() == null) { + return null; + } + return new TypeDescriptor(type, null, getAnnotations()); + } + @Nullable private TypeDescriptor narrow(@Nullable Object value, @Nullable TypeDescriptor typeDescriptor) { if (typeDescriptor != null) { @@ -475,7 +508,7 @@ else if (isMap()) { ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor())); } else { - return true; + return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics()); } } @@ -645,7 +678,7 @@ public static TypeDescriptor nested(MethodParameter methodParameter, int nesting throw new IllegalArgumentException("MethodParameter nesting level must be 1: " + "use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); } - return nested(new TypeDescriptor(methodParameter), nestingLevel); + return new TypeDescriptor(methodParameter).nested(nestingLevel); } /** @@ -671,7 +704,7 @@ public static TypeDescriptor nested(MethodParameter methodParameter, int nesting */ @Nullable public static TypeDescriptor nested(Field field, int nestingLevel) { - return nested(new TypeDescriptor(field), nestingLevel); + return new TypeDescriptor(field).nested(nestingLevel); } /** @@ -697,33 +730,7 @@ public static TypeDescriptor nested(Field field, int nestingLevel) { */ @Nullable public static TypeDescriptor nested(Property property, int nestingLevel) { - return nested(new TypeDescriptor(property), nestingLevel); - } - - @Nullable - private static TypeDescriptor nested(TypeDescriptor typeDescriptor, int nestingLevel) { - ResolvableType nested = typeDescriptor.resolvableType; - for (int i = 0; i < nestingLevel; i++) { - if (Object.class == nested.getType()) { - // Could be a collection type but we don't know about its element type, - // so let's just assume there is an element type of type Object... - } - else { - nested = nested.getNested(2); - } - } - if (nested == ResolvableType.NONE) { - return null; - } - return getRelatedIfResolvable(typeDescriptor, nested); - } - - @Nullable - private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, ResolvableType type) { - if (type.resolve() == null) { - return null; - } - return new TypeDescriptor(type, null, source.getAnnotations()); + return new TypeDescriptor(property).nested(nestingLevel); } @@ -733,18 +740,26 @@ private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, Reso * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) */ - private class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + + private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]); - @Nullable private final Annotation[] annotations; - public AnnotatedElementAdapter(@Nullable Annotation[] annotations) { + private AnnotatedElementAdapter(Annotation[] annotations) { this.annotations = annotations; } + private static AnnotatedElementAdapter from(@Nullable Annotation[] annotations) { + if (annotations == null || annotations.length == 0) { + return EMPTY; + } + return new AnnotatedElementAdapter(annotations); + } + @Override public boolean isAnnotationPresent(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { + for (Annotation annotation : this.annotations) { if (annotation.annotationType() == annotationClass) { return true; } @@ -756,7 +771,7 @@ public boolean isAnnotationPresent(Class annotationClass) @Nullable @SuppressWarnings("unchecked") public T getAnnotation(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { + for (Annotation annotation : this.annotations) { if (annotation.annotationType() == annotationClass) { return (T) annotation; } @@ -766,7 +781,7 @@ public T getAnnotation(Class annotationClass) { @Override public Annotation[] getAnnotations() { - return (this.annotations != null ? this.annotations.clone() : EMPTY_ANNOTATION_ARRAY); + return (isEmpty() ? this.annotations : this.annotations.clone()); } @Override @@ -775,7 +790,7 @@ public Annotation[] getDeclaredAnnotations() { } public boolean isEmpty() { - return ObjectUtils.isEmpty(this.annotations); + return (this.annotations.length == 0); } @Override @@ -791,7 +806,7 @@ public int hashCode() { @Override public String toString() { - return TypeDescriptor.this.toString(); + return Arrays.toString(this.annotations); } } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java index 7c345d1c76d9..10307745227a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ * * @author Keith Donald * @author Phillip Webb + * @author Sam Brannen * @since 3.0 */ final class ArrayToArrayConverter implements ConditionalGenericConverter { @@ -64,8 +65,8 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (this.conversionService instanceof GenericConversionService genericConversionService) { TypeDescriptor targetElement = targetType.getElementTypeDescriptor(); - if (targetElement != null && genericConversionService.canBypassConvert( - sourceType.getElementTypeDescriptor(), targetElement)) { + if (targetElement != null && targetType.getType().isInstance(source) && + genericConversionService.canBypassConvert(sourceType.getElementTypeDescriptor(), targetElement)) { return source; } } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java index ea96d762c664..7a9c76239da9 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.core.convert.support; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Set; @@ -69,7 +70,7 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDe int length = Array.getLength(source); TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); - Collection target = CollectionFactory.createCollection(targetType.getType(), + Collection target = createCollection(targetType.getType(), (elementDesc != null ? elementDesc.getType() : null), length); if (elementDesc == null) { @@ -89,4 +90,13 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDe return target; } + private Collection createCollection(Class targetType, @Nullable Class elementType, int length) { + if (targetType.isInterface() && targetType.isAssignableFrom(ArrayList.class)) { + // Source is an array -> prefer ArrayList for Collection and SequencedCollection. + // CollectionFactory.createCollection traditionally prefers LinkedHashSet instead. + return new ArrayList<>(length); + } + return CollectionFactory.createCollection(targetType, elementType, length); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java index dfd95ca3788c..35a70baaa109 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import java.util.Currency; import java.util.Locale; import java.util.UUID; +import java.util.regex.Pattern; +import org.springframework.core.KotlinDetector; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.lang.Nullable; @@ -166,6 +168,14 @@ private static void addScalarConverters(ConverterRegistry converterRegistry) { converterRegistry.addConverter(new StringToUUIDConverter()); converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new StringToPatternConverter()); + converterRegistry.addConverter(Pattern.class, String.class, new ObjectToStringConverter()); + + if (KotlinDetector.isKotlinPresent()) { + converterRegistry.addConverter(new StringToRegexConverter()); + converterRegistry.addConverter(kotlin.text.Regex.class, String.class, new ObjectToStringConverter()); + } } } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index d2d50eac6ad0..a662918b641c 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.core.convert.support; -import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; @@ -31,7 +30,6 @@ import org.springframework.core.DecoratingProxy; import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; @@ -140,11 +138,7 @@ public boolean canConvert(@Nullable Class sourceType, Class targetType) { @Override public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { Assert.notNull(targetType, "Target type to convert to cannot be null"); - if (sourceType == null) { - return true; - } - GenericConverter converter = getConverter(sourceType, targetType); - return (converter != null); + return (sourceType == null || getConverter(sourceType, targetType) != null); } /** @@ -160,15 +154,11 @@ public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor ta */ public boolean canBypassConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { Assert.notNull(targetType, "Target type to convert to cannot be null"); - if (sourceType == null) { - return true; - } - GenericConverter converter = getConverter(sourceType, targetType); - return (converter == NO_OP_CONVERTER); + return (sourceType == null || getConverter(sourceType, targetType) == NO_OP_CONVERTER); } - @Override @SuppressWarnings("unchecked") + @Override @Nullable public T convert(@Nullable Object source, Class targetType) { Assert.notNull(targetType, "Target type to convert to cannot be null"); @@ -195,23 +185,6 @@ public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceTy return handleConverterNotFound(source, sourceType, targetType); } - /** - * Convenience operation for converting a source object to the specified targetType, - * where the target type is a descriptor that provides additional conversion context. - * Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and - * encapsulates the construction of the source type descriptor using - * {@link TypeDescriptor#forObject(Object)}. - * @param source the source object - * @param targetType the target type - * @return the converted value - * @throws ConversionException if a conversion exception occurred - * @throws IllegalArgumentException if targetType is {@code null} - */ - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor targetType) { - return convert(source, TypeDescriptor.forObject(source), targetType); - } - @Override public String toString() { return this.converters.toString(); @@ -578,7 +551,7 @@ private List> getClassHierarchy(Class type) { int i = 0; while (i < hierarchy.size()) { Class candidate = hierarchy.get(i); - candidate = (array ? candidate.getComponentType() : ClassUtils.resolvePrimitiveIfNecessary(candidate)); + candidate = (array ? candidate.componentType() : ClassUtils.resolvePrimitiveIfNecessary(candidate)); Class superclass = candidate.getSuperclass(); if (superclass != null && superclass != Object.class && superclass != Enum.class) { addToClassHierarchy(i + 1, candidate.getSuperclass(), array, hierarchy, visited); @@ -588,9 +561,8 @@ private List> getClassHierarchy(Class type) { } if (Enum.class.isAssignableFrom(type)) { - addToClassHierarchy(hierarchy.size(), Enum.class, array, hierarchy, visited); addToClassHierarchy(hierarchy.size(), Enum.class, false, hierarchy, visited); - addInterfacesToClassHierarchy(Enum.class, array, hierarchy, visited); + addInterfacesToClassHierarchy(Enum.class, false, hierarchy, visited); } addToClassHierarchy(hierarchy.size(), Object.class, array, hierarchy, visited); @@ -610,7 +582,7 @@ private void addToClassHierarchy(int index, Class type, boolean asArray, List> hierarchy, Set> visited) { if (asArray) { - type = Array.newInstance(type, 0).getClass(); + type = type.arrayType(); } if (visited.add(type)) { hierarchy.add(index, type); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java index 5b7cccba95f0..b65f11f24f0a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -136,7 +136,7 @@ static boolean hasConversionMethodOrConstructor(Class targetClass, Class s @Nullable private static Executable getValidatedExecutable(Class targetClass, Class sourceClass) { Executable executable = conversionExecutableCache.get(targetClass); - if (isApplicable(executable, sourceClass)) { + if (executable != null && isApplicable(executable, sourceClass)) { return executable; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java index 038a82d7423a..7cece80cf880 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ /** * Converts a comma-delimited String to an Array. - * Only matches if String.class can be converted to the target array element type. + * Only matches if {@code String.class} can be converted to the target array element type. * * @author Keith Donald * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToPatternConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPatternConverter.java new file mode 100644 index 000000000000..ca2634b2117b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPatternConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.support; + +import java.util.regex.Pattern; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; + +/** + * Converts from a String to a {@link java.util.regex.Pattern}. + * + * @author Valery Yatsynovich + * @author Stephane Nicoll + * @since 6.1 + */ +final class StringToPatternConverter implements Converter { + + @Override + @Nullable + public Pattern convert(String source) { + if (source.isEmpty()) { + return null; + } + return Pattern.compile(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToRegexConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToRegexConverter.java new file mode 100644 index 000000000000..10a1b93d1981 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToRegexConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.support; + +import kotlin.text.Regex; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; + +/** + * Converts from a String to a Kotlin {@link Regex}. + * + * @author Stephane Nicoll + * @author Sebastien Deleuze + * @since 6.1 + */ +final class StringToRegexConverter implements Converter { + + @Override + @Nullable + public Regex convert(String source) { + if (source.isEmpty()) { + return null; + } + return new Regex(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java index 969634aeda7b..e2cc3355c126 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java @@ -66,31 +66,31 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore"; /** - * Name of property to set to specify active profiles: {@value}. Value may be comma - * delimited. + * Name of the property to set to specify active profiles: {@value}. + *

    The value may be comma delimited. *

    Note that certain shell environments such as Bash disallow the use of the period * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} - * is in use, this property may be specified as an environment variable as + * is in use, this property may be specified as an environment variable named * {@code SPRING_PROFILES_ACTIVE}. * @see ConfigurableEnvironment#setActiveProfiles */ public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active"; /** - * Name of property to set to specify profiles active by default: {@value}. Value may - * be comma delimited. + * Name of the property to set to specify profiles that are active by default: {@value}. + *

    The value may be comma delimited. *

    Note that certain shell environments such as Bash disallow the use of the period * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} - * is in use, this property may be specified as an environment variable as + * is in use, this property may be specified as an environment variable named * {@code SPRING_PROFILES_DEFAULT}. * @see ConfigurableEnvironment#setDefaultProfiles */ public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default"; /** - * Name of reserved default profile name: {@value}. If no default profile names are - * explicitly set and no active profile names are explicitly set, this profile will - * automatically be activated by default. + * Name of the reserved default profile name: {@value}. + *

    If no default profile names are explicitly set and no active profile names + * are explicitly set, this profile will automatically be activated by default. * @see #getReservedDefaultProfiles * @see ConfigurableEnvironment#setDefaultProfiles * @see ConfigurableEnvironment#setActiveProfiles diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java index c317f5e2dc72..6a5cd246a25d 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java @@ -25,9 +25,8 @@ /** * Abstract base class for {@link PropertySource} implementations backed by command line * arguments. The parameterized type {@code T} represents the underlying source of command - * line options. This may be as simple as a String array in the case of - * {@link SimpleCommandLinePropertySource}, or specific to a particular API such as JOpt's - * {@code OptionSet} in the case of {@link JOptCommandLinePropertySource}. + * line options. For instance, {@link SimpleCommandLinePropertySource} uses a String + * array. * *

    Purpose and General Usage

    * @@ -203,7 +202,6 @@ * @param the source type * @see PropertySource * @see SimpleCommandLinePropertySource - * @see JOptCommandLinePropertySource */ public abstract class CommandLinePropertySource extends EnumerablePropertySource { diff --git a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java index 6224a95f3394..4c0553f0e4a3 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package org.springframework.core.env; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -78,15 +78,20 @@ public boolean containsProperty(String name) { @Override public String[] getPropertyNames() { - Set names = new LinkedHashSet<>(); + List namesList = new ArrayList<>(this.propertySources.size()); + int total = 0; for (PropertySource propertySource : this.propertySources) { if (!(propertySource instanceof EnumerablePropertySource enumerablePropertySource)) { throw new IllegalStateException( "Failed to enumerate property names due to non-enumerable property source: " + propertySource); } - names.addAll(Arrays.asList(enumerablePropertySource.getPropertyNames())); + String[] names = enumerablePropertySource.getPropertyNames(); + namesList.add(names); + total += names.length; } - return StringUtils.toStringArray(names); + Set allNames = new LinkedHashSet<>(total); + namesList.forEach(names -> Collections.addAll(allNames, names)); + return StringUtils.toStringArray(allNames); } diff --git a/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java index 2eb456b17602..679bca5b1438 100644 --- a/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java @@ -61,7 +61,9 @@ * @see CommandLinePropertySource * @see joptsimple.OptionParser * @see joptsimple.OptionSet + * @deprecated since 6.1 with no plans for a replacement */ +@Deprecated(since = "6.1") public class JOptCommandLinePropertySource extends CommandLinePropertySource { /** diff --git a/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java index 0edb4fe0c37c..bedb7918c15f 100644 --- a/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java +++ b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.util.Iterator; import java.util.List; import java.util.Spliterator; -import java.util.Spliterators; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Stream; @@ -69,7 +68,7 @@ public Iterator> iterator() { @Override public Spliterator> spliterator() { - return Spliterators.spliterator(this.propertySourceList, 0); + return this.propertySourceList.spliterator(); } @Override diff --git a/spring-core/src/main/java/org/springframework/core/env/Profiles.java b/spring-core/src/main/java/org/springframework/core/env/Profiles.java index 340bb2b181cf..2c2800155a68 100644 --- a/spring-core/src/main/java/org/springframework/core/env/Profiles.java +++ b/spring-core/src/main/java/org/springframework/core/env/Profiles.java @@ -28,17 +28,19 @@ * @author Phillip Webb * @author Sam Brannen * @since 5.1 + * @see Environment#acceptsProfiles(Profiles) + * @see Environment#matchesProfiles(String...) */ @FunctionalInterface public interface Profiles { /** * Test if this {@code Profiles} instance matches against the given - * active profiles predicate. - * @param activeProfiles a predicate that tests whether a given profile is + * predicate. + * @param isProfileActive a predicate that tests whether a given profile is * currently active */ - boolean matches(Predicate activeProfiles); + boolean matches(Predicate isProfileActive); /** diff --git a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java index 9f5868d248cd..dcc9474e5dd3 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java +++ b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java @@ -24,10 +24,10 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Internal parser used by {@link Profiles#of}. @@ -180,7 +180,14 @@ public int hashCode() { @Override public String toString() { - return StringUtils.collectionToDelimitedString(this.expressions, " or "); + if (this.expressions.size() == 1) { + return this.expressions.iterator().next(); + } + return this.expressions.stream().map(this::wrap).collect(Collectors.joining(" | ")); + } + + private String wrap(String str) { + return "(" + str + ")"; } } diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java index d09c3683510e..9741c5819287 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ protected PropertiesPropertySource(String name, Map source) { @Override public String[] getPropertyNames() { synchronized (this.source) { - return super.getPropertyNames(); + return ((Map) this.source).keySet().stream().filter(k -> k instanceof String).toArray(String[]::new); } } diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java index 64ad2c0d9283..3135f0722001 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java @@ -16,6 +16,8 @@ package org.springframework.core.env; +import java.util.Objects; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -147,7 +149,7 @@ public boolean equals(@Nullable Object other) { */ @Override public int hashCode() { - return ObjectUtils.nullSafeHashCode(getName()); + return Objects.hashCode(getName()); } /** diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java index c4f34a13e504..3efb89a327bf 100644 --- a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,15 +74,13 @@ * *

    Beyond the basics

    * - *

    When more fully-featured command line parsing is necessary, consider using - * the provided {@link JOptCommandLinePropertySource}, or implement your own - * {@code CommandLinePropertySource} against the command line parsing library of your - * choice. + *

    When more fully-featured command line parsing is necessary, consider + * implementing your own {@code CommandLinePropertySource} against the command line + * parsing library of your choice. * * @author Chris Beams * @since 3.1 * @see CommandLinePropertySource - * @see JOptCommandLinePropertySource */ public class SimpleCommandLinePropertySource extends CommandLinePropertySource { diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java index a12258a24e69..1454b92f8057 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java @@ -100,9 +100,13 @@ public ClassPathResource(String path, @Nullable ClassLoader classLoader) { * the class path via a leading slash. *

    If the supplied {@code Class} is {@code null}, the default class * loader will be used for loading the resource. + *

    This is also useful for resource access within the module system, + * loading a resource from the containing module of a given {@code Class}. + * See {@link ModuleResource} and its javadoc. * @param path relative or absolute path within the class path * @param clazz the class to load resources with * @see ClassUtils#getDefaultClassLoader() + * @see ModuleResource */ public ClassPathResource(String path, @Nullable Class clazz) { Assert.notNull(path, "Path must not be null"); diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java index ab8e393b48d0..906eb233a6c6 100644 --- a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,16 +23,29 @@ import org.springframework.util.Assert; /** - * {@link Resource} implementation for a given {@link InputStream}. + * {@link Resource} implementation for a given {@link InputStream} or a given + * {@link InputStreamSource} (which can be supplied as a lambda expression) + * for a lazy {@link InputStream} on demand. + * *

    Should only be used if no other specific {@code Resource} implementation * is applicable. In particular, prefer {@link ByteArrayResource} or any of the - * file-based {@code Resource} implementations where possible. + * file-based {@code Resource} implementations if possible. If you need to obtain + * a custom stream multiple times, use a custom {@link AbstractResource} subclass + * with a corresponding {@code getInputStream()} implementation. * *

    In contrast to other {@code Resource} implementations, this is a descriptor * for an already opened resource - therefore returning {@code true} from - * {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to - * keep the resource descriptor somewhere, or if you need to read from a stream - * multiple times. + * {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to keep + * the resource descriptor somewhere, or if you need to read from a stream multiple + * times. This also applies when constructed with an {@code InputStreamSource} + * which lazily obtains the stream but only allows for single access as well. + * + *

    NOTE: This class does not provide an independent {@link #contentLength()} + * implementation: Any such call will consume the given {@code InputStream}! + * Consider overriding {@code #contentLength()} with a custom implementation if + * possible. For any other purpose, it is not recommended to extend from this + * class; this is particularly true when used with Spring's web resource rendering + * which specifically skips {@code #contentLength()} for this exact class only. * * @author Juergen Hoeller * @author Sam Brannen @@ -44,30 +57,62 @@ */ public class InputStreamResource extends AbstractResource { - private final InputStream inputStream; + private final InputStreamSource inputStreamSource; private final String description; + private final Object equality; + private boolean read = false; /** - * Create a new InputStreamResource. + * Create a new {@code InputStreamResource} with a lazy {@code InputStream} + * for single use. + * @param inputStreamSource an on-demand source for a single-use InputStream + * @since 6.1.7 + */ + public InputStreamResource(InputStreamSource inputStreamSource) { + this(inputStreamSource, "resource loaded from InputStreamSource"); + } + + /** + * Create a new {@code InputStreamResource} with a lazy {@code InputStream} + * for single use. + * @param inputStreamSource an on-demand source for a single-use InputStream + * @param description where the InputStream comes from + * @since 6.1.7 + */ + public InputStreamResource(InputStreamSource inputStreamSource, @Nullable String description) { + Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); + this.inputStreamSource = inputStreamSource; + this.description = (description != null ? description : ""); + this.equality = inputStreamSource; + } + + /** + * Create a new {@code InputStreamResource} for an existing {@code InputStream}. + *

    Consider retrieving the InputStream on demand if possible, reducing its + * lifetime and reliably opening it and closing it through regular + * {@link InputStreamSource#getInputStream()} usage. * @param inputStream the InputStream to use + * @see #InputStreamResource(InputStreamSource) */ public InputStreamResource(InputStream inputStream) { this(inputStream, "resource loaded through InputStream"); } /** - * Create a new InputStreamResource. + * Create a new {@code InputStreamResource} for an existing {@code InputStream}. * @param inputStream the InputStream to use * @param description where the InputStream comes from + * @see #InputStreamResource(InputStreamSource, String) */ public InputStreamResource(InputStream inputStream, @Nullable String description) { Assert.notNull(inputStream, "InputStream must not be null"); - this.inputStream = inputStream; + this.inputStreamSource = () -> inputStream; this.description = (description != null ? description : ""); + this.equality = inputStream; } @@ -94,11 +139,11 @@ public boolean isOpen() { @Override public InputStream getInputStream() throws IOException, IllegalStateException { if (this.read) { - throw new IllegalStateException("InputStream has already been read - " + - "do not use InputStreamResource if a stream needs to be read multiple times"); + throw new IllegalStateException("InputStream has already been read (possibly for early content length " + + "determination) - do not use InputStreamResource if a stream needs to be read multiple times"); } this.read = true; - return this.inputStream; + return this.inputStreamSource.getInputStream(); } /** @@ -117,7 +162,7 @@ public String getDescription() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof InputStreamResource that && - this.inputStream.equals(that.inputStream))); + this.equality.equals(that.equality))); } /** @@ -125,7 +170,7 @@ public boolean equals(@Nullable Object other) { */ @Override public int hashCode() { - return this.inputStream.hashCode(); + return this.equality.hashCode(); } } diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java index 8d72c9cd8bbc..2f3eda75d793 100644 --- a/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,11 +38,12 @@ * @see InputStreamResource * @see ByteArrayResource */ +@FunctionalInterface public interface InputStreamSource { /** * Return an {@link InputStream} for the content of an underlying resource. - *

    It is expected that each call creates a fresh stream. + *

    It is usually expected that every such call creates a fresh stream. *

    This requirement is particularly important when you consider an API such * as JavaMail, which needs to be able to read the stream multiple times when * creating mail attachments. For such a use case, it is required @@ -51,6 +52,7 @@ public interface InputStreamSource { * @throws java.io.FileNotFoundException if the underlying resource does not exist * @throws IOException if the content stream could not be opened * @see Resource#isReadable() + * @see Resource#isOpen() */ InputStream getInputStream() throws IOException; diff --git a/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java b/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java new file mode 100644 index 000000000000..225a6042a397 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.io; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for {@link java.lang.Module} resolution, + * performing {@link #getInputStream()} access via {@link Module#getResourceAsStream}. + * + *

    Alternatively, consider accessing resources in a module path layout via + * {@link ClassPathResource} for exported resources, or specifically relative to + * a {@code Class} via {@link ClassPathResource#ClassPathResource(String, Class)} + * for local resolution within the containing module of that specific class. + * In common scenarios, module resources will simply be transparently visible as + * classpath resources and therefore do not need any special treatment at all. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 6.1 + * @see Module#getResourceAsStream + * @see ClassPathResource + */ +public class ModuleResource extends AbstractResource { + + private final Module module; + + private final String path; + + + /** + * Create a new {@code ModuleResource} for the given {@link Module} + * and the given resource path. + * @param module the runtime module to search within + * @param path the resource path within the module + */ + public ModuleResource(Module module, String path) { + Assert.notNull(module, "Module must not be null"); + Assert.notNull(path, "Path must not be null"); + this.module = module; + this.path = path; + } + + + /** + * Return the {@link Module} for this resource. + */ + public final Module getModule() { + return this.module; + } + + /** + * Return the path for this resource. + */ + public final String getPath() { + return this.path; + } + + + @Override + public InputStream getInputStream() throws IOException { + InputStream is = this.module.getResourceAsStream(this.path); + if (is == null) { + throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); + } + return is; + } + + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return new ModuleResource(this.module, pathToUse); + } + + @Override + @Nullable + public String getFilename() { + return StringUtils.getFilename(this.path); + } + + @Override + public String getDescription() { + return "module resource [" + this.path + "]" + + (this.module.isNamed() ? " from module [" + this.module.getName() + "]" : ""); + } + + + @Override + public boolean equals(@Nullable Object obj) { + return (this == obj || (obj instanceof ModuleResource that && + this.module.equals(that.module) && this.path.equals(that.path))); + } + + @Override + public int hashCode() { + return this.module.hashCode() * 31 + this.path.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java index a7a1f1dc43bc..91458934fed3 100644 --- a/spring-core/src/main/java/org/springframework/core/io/Resource.java +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -152,8 +152,7 @@ default byte[] getContentAsByteArray() throws IOException { } /** - * Returns the contents of this resource as a string, using the specified - * charset. + * Return the contents of this resource as a string, using the specified charset. * @param charset the charset to use for decoding * @return the contents of this resource as a {@code String} * @throws java.io.FileNotFoundException if the resource cannot be resolved as diff --git a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java index 2a6bd335e1ca..1a9e6d5dd7c8 100644 --- a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import java.net.URLConnection; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.util.Base64; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -46,6 +47,9 @@ */ public class UrlResource extends AbstractFileResolvingResource { + private static final String AUTHORIZATION = "Authorization"; + + /** * Original URI, if available; used for URI and File access. */ @@ -89,33 +93,31 @@ public UrlResource(URI uri) throws MalformedURLException { } /** - * Create a new {@code UrlResource} based on a URL path. + * Create a new {@code UrlResource} based on a URI path. *

    Note: The given path needs to be pre-encoded if necessary. - * @param path a URL path - * @throws MalformedURLException if the given URL path is not valid - * @see java.net.URL#URL(String) + * @param path a URI path + * @throws MalformedURLException if the given URI path is not valid + * @see ResourceUtils#toURI(String) */ public UrlResource(String path) throws MalformedURLException { Assert.notNull(path, "Path must not be null"); + String cleanedPath = StringUtils.cleanPath(path); + URI uri; + URL url; - // Equivalent without java.net.URL constructor - for building on JDK 20+ - /* try { - String cleanedPath = StringUtils.cleanPath(path); - this.uri = ResourceUtils.toURI(cleanedPath); - this.url = this.uri.toURL(); - this.cleanedUrl = cleanedPath; + // Prefer URI construction with toURL conversion (as of 6.1) + uri = ResourceUtils.toURI(cleanedPath); + url = uri.toURL(); } catch (URISyntaxException | IllegalArgumentException ex) { - MalformedURLException exToThrow = new MalformedURLException(ex.getMessage()); - exToThrow.initCause(ex); - throw exToThrow; + uri = null; + url = ResourceUtils.toURL(path); } - */ - this.uri = null; - this.url = ResourceUtils.toURL(path); - this.cleanedUrl = StringUtils.cleanPath(path); + this.uri = uri; + this.url = url; + this.cleanedUrl = cleanedPath; } /** @@ -128,7 +130,7 @@ public UrlResource(String path) throws MalformedURLException { * @throws MalformedURLException if the given URL specification is not valid * @see java.net.URI#URI(String, String, String) */ - public UrlResource(String protocol, String location) throws MalformedURLException { + public UrlResource(String protocol, String location) throws MalformedURLException { this(protocol, location, null); } @@ -144,7 +146,7 @@ public UrlResource(String protocol, String location) throws MalformedURLExceptio * @throws MalformedURLException if the given URL specification is not valid * @see java.net.URI#URI(String, String, String) */ - public UrlResource(String protocol, String location, @Nullable String fragment) throws MalformedURLException { + public UrlResource(String protocol, String location, @Nullable String fragment) throws MalformedURLException { try { this.uri = new URI(protocol, location, fragment); this.url = this.uri.toURL(); @@ -239,6 +241,16 @@ public InputStream getInputStream() throws IOException { } } + @Override + protected void customizeConnection(URLConnection con) throws IOException { + super.customizeConnection(con); + String userInfo = this.url.getUserInfo(); + if (userInfo != null) { + String encodedCredentials = Base64.getUrlEncoder().encodeToString(userInfo.getBytes()); + con.setRequestProperty(AUTHORIZATION, "Basic " + encodedCredentials); + } + } + /** * This implementation returns the underlying URL reference. */ @@ -299,7 +311,8 @@ public Resource createRelative(String relativePath) throws MalformedURLException /** * This delegate creates a {@code java.net.URL}, applying the given path * relative to the path of the underlying URL of this resource descriptor. - * A leading slash will get dropped; a "#" symbol will get encoded. + *

    A leading slash will get dropped; a "#" symbol will get encoded. + * Note that this method effectively cleans the combined path as of 6.1. * @since 5.2 * @see #createRelative(String) * @see ResourceUtils#toRelativeURL(URL, String) @@ -321,13 +334,15 @@ protected URL createRelativeURL(String relativePath) throws MalformedURLExceptio @Nullable public String getFilename() { if (this.uri != null) { - // URI path is decoded and has standard separators - return StringUtils.getFilename(this.uri.getPath()); - } - else { - String filename = StringUtils.getFilename(StringUtils.cleanPath(this.url.getPath())); - return (filename != null ? URLDecoder.decode(filename, StandardCharsets.UTF_8) : null); + String path = this.uri.getPath(); + if (path != null) { + // Prefer URI path: decoded and has standard separators + return StringUtils.getFilename(this.uri.getPath()); + } } + // Otherwise, process URL path + String filename = StringUtils.getFilename(StringUtils.cleanPath(this.url.getPath())); + return (filename != null ? URLDecoder.decode(filename, StandardCharsets.UTF_8) : null); } /** diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index b4360235179f..94521587035e 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -263,29 +263,35 @@ default DataBuffer ensureCapacity(int capacity) { default DataBuffer write(CharSequence charSequence, Charset charset) { Assert.notNull(charSequence, "CharSequence must not be null"); Assert.notNull(charset, "Charset must not be null"); - if (charSequence.length() > 0) { + if (!charSequence.isEmpty()) { CharsetEncoder encoder = charset.newEncoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE); CharBuffer src = CharBuffer.wrap(charSequence); - int cap = (int) (src.remaining() * encoder.averageBytesPerChar()); + int averageSize = (int) Math.ceil(src.remaining() * encoder.averageBytesPerChar()); + ensureWritable(averageSize); while (true) { - ensureWritable(cap); CoderResult cr; - try (ByteBufferIterator iterator = writableByteBuffers()) { - Assert.state(iterator.hasNext(), "No ByteBuffer available"); - ByteBuffer dest = iterator.next(); - cr = encoder.encode(src, dest, true); - if (cr.isUnderflow()) { - cr = encoder.flush(dest); + if (src.hasRemaining()) { + try (ByteBufferIterator iterator = writableByteBuffers()) { + Assert.state(iterator.hasNext(), "No ByteBuffer available"); + ByteBuffer dest = iterator.next(); + cr = encoder.encode(src, dest, true); + if (cr.isUnderflow()) { + cr = encoder.flush(dest); + } + writePosition(writePosition() + dest.position()); } - writePosition(dest.position()); + } + else { + cr = CoderResult.UNDERFLOW; } if (cr.isUnderflow()) { break; } - if (cr.isOverflow()) { - cap = 2 * cap + 1; + else if (cr.isOverflow()) { + int maxSize = (int) Math.ceil(src.remaining() * encoder.maxBytesPerChar()); + ensureWritable(maxSize); } } } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index efc1a8a82770..caee77b2c0bf 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -41,6 +42,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; @@ -62,10 +64,12 @@ */ public abstract class DataBufferUtils { - private final static Log logger = LogFactory.getLog(DataBufferUtils.class); + private static final Log logger = LogFactory.getLog(DataBufferUtils.class); private static final Consumer RELEASE_CONSUMER = DataBufferUtils::release; + private static final int DEFAULT_CHUNK_SIZE = 1024; + //--------------------------------------------------------------------- // Reading @@ -405,6 +409,85 @@ static void closeChannel(@Nullable Channel channel) { } + /** + * Create a new {@code Publisher} based on bytes written to a + * {@code OutputStream}. + *

      + *
    • The parameter {@code outputStreamConsumer} is invoked once per + * subscription of the returned {@code Publisher}, when the first + * item is + * {@linkplain Subscription#request(long) requested}.
    • + *
    • {@link OutputStream#write(byte[], int, int) OutputStream.write()} + * invocations made by {@code outputStreamConsumer} are buffered until they + * exceed the default chunk size of 1024, or when the stream is + * {@linkplain OutputStream#flush() flushed} and then result in a + * {@linkplain Subscriber#onNext(Object) published} item + * if there is {@linkplain Subscription#request(long) demand}.
    • + *
    • If there is no demand, {@code OutputStream.write()} will block + * until there is.
    • + *
    • If the subscription is {@linkplain Subscription#cancel() cancelled}, + * {@code OutputStream.write()} will throw a {@code IOException}.
    • + *
    • The subscription is + * {@linkplain Subscriber#onComplete() completed} when + * {@code outputStreamHandler} completes.
    • + *
    • Any exceptions thrown from {@code outputStreamHandler} will + * be dispatched to the {@linkplain Subscriber#onError(Throwable) Subscriber}. + *
    + * @param outputStreamConsumer invoked when the first buffer is requested + * @param executor used to invoke the {@code outputStreamHandler} + * @return a {@code Publisher} based on bytes written by + * {@code outputStreamHandler} + * @since 6.1 + */ + public static Publisher outputStreamPublisher(Consumer outputStreamConsumer, + DataBufferFactory bufferFactory, Executor executor) { + + return outputStreamPublisher(outputStreamConsumer, bufferFactory, executor, DEFAULT_CHUNK_SIZE); + } + + /** + * Creates a new {@code Publisher} based on bytes written to a + * {@code OutputStream}. + *
      + *
    • The parameter {@code outputStreamConsumer} is invoked once per + * subscription of the returned {@code Publisher}, when the first + * item is + * {@linkplain Subscription#request(long) requested}.
    • + *
    • {@link OutputStream#write(byte[], int, int) OutputStream.write()} + * invocations made by {@code outputStreamHandler} are buffered until they + * reach or exceed {@code chunkSize}, or when the stream is + * {@linkplain OutputStream#flush() flushed} and then result in a + * {@linkplain Subscriber#onNext(Object) published} item + * if there is {@linkplain Subscription#request(long) demand}.
    • + *
    • If there is no demand, {@code OutputStream.write()} will block + * until there is.
    • + *
    • If the subscription is {@linkplain Subscription#cancel() cancelled}, + * {@code OutputStream.write()} will throw a {@code IOException}.
    • + *
    • The subscription is + * {@linkplain Subscriber#onComplete() completed} when + * {@code outputStreamHandler} completes.
    • + *
    • Any exceptions thrown from {@code outputStreamHandler} will + * be dispatched to the {@linkplain Subscriber#onError(Throwable) Subscriber}. + *
    + * @param outputStreamConsumer invoked when the first buffer is requested + * @param executor used to invoke the {@code outputStreamHandler} + * @param chunkSize minimum size of the buffer produced by the publisher + * @return a {@code Publisher} based on bytes written by + * {@code outputStreamHandler} + * @since 6.1 + */ + public static Publisher outputStreamPublisher(Consumer outputStreamConsumer, + DataBufferFactory bufferFactory, Executor executor, int chunkSize) { + + Assert.notNull(outputStreamConsumer, "OutputStreamConsumer must not be null"); + Assert.notNull(bufferFactory, "BufferFactory must not be null"); + Assert.notNull(executor, "Executor must not be null"); + Assert.isTrue(chunkSize > 0, "Chunk size must be > 0"); + + return new OutputStreamPublisher(outputStreamConsumer, bufferFactory, executor, chunkSize); + } + + //--------------------------------------------------------------------- // Various //--------------------------------------------------------------------- @@ -745,7 +828,7 @@ private interface NestedMatcher extends Matcher { */ private static class SingleByteMatcher implements NestedMatcher { - static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); + static final SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); private final byte[] delimiter; @@ -784,7 +867,7 @@ public void reset() { /** * Base class for a {@link NestedMatcher}. */ - private static abstract class AbstractNestedMatcher implements NestedMatcher { + private abstract static class AbstractNestedMatcher implements NestedMatcher { private final byte[] delimiter; @@ -990,7 +1073,7 @@ private void read() { DataBuffer.ByteBufferIterator iterator = dataBuffer.writableByteBuffers(); Assert.state(iterator.hasNext(), "No ByteBuffer available"); ByteBuffer byteBuffer = iterator.next(); - Attachment attachment = new Attachment(dataBuffer, iterator); + Attachment attachment = new Attachment(dataBuffer, iterator); this.channel.read(byteBuffer, this.position.get(), attachment, this); } @@ -999,7 +1082,7 @@ public void completed(Integer read, Attachment attachment) { attachment.iterator().close(); DataBuffer dataBuffer = attachment.dataBuffer(); - if (this.state.get().equals(State.DISPOSED)) { + if (this.state.get() == State.DISPOSED) { release(dataBuffer); closeChannel(this.channel); return; @@ -1030,13 +1113,13 @@ public void completed(Integer read, Attachment attachment) { } @Override - public void failed(Throwable exc, Attachment attachment) { + public void failed(Throwable ex, Attachment attachment) { attachment.iterator().close(); release(attachment.dataBuffer()); closeChannel(this.channel); this.state.set(State.DISPOSED); - this.sink.error(exc); + this.sink.error(ex); } private enum State { @@ -1095,7 +1178,6 @@ protected void hookOnComplete() { public Context currentContext() { return Context.of(this.sink.contextView()); } - } @@ -1190,13 +1272,13 @@ else if (this.completed.get()) { } @Override - public void failed(Throwable exc, Attachment attachment) { + public void failed(Throwable ex, Attachment attachment) { attachment.iterator().close(); this.sink.next(attachment.dataBuffer()); this.writing.set(false); - this.sink.error(exc); + this.sink.error(ex); } @Override @@ -1205,9 +1287,6 @@ public Context currentContext() { } private record Attachment(ByteBuffer byteBuffer, DataBuffer dataBuffer, DataBuffer.ByteBufferIterator iterator) {} - - } - } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index def3ccf39649..8d9fcda64354 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -371,7 +371,7 @@ public DataBuffer split(int index) { .slice(); this.writePosition = Math.max(this.writePosition, index) - index; this.readPosition = Math.max(this.readPosition, index) - index; - capacity(this.byteBuffer.capacity()); + this.capacity = this.byteBuffer.capacity(); return result; } @@ -416,16 +416,15 @@ public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { @Override public DataBuffer.ByteBufferIterator readableByteBuffers() { - ByteBuffer readOnly = this.byteBuffer.asReadOnlyBuffer(); - readOnly.clear().position(this.readPosition).limit(this.writePosition - this.readPosition); + ByteBuffer readOnly = this.byteBuffer.slice(this.readPosition, readableByteCount()) + .asReadOnlyBuffer(); return new ByteBufferIterator(readOnly); } @Override public DataBuffer.ByteBufferIterator writableByteBuffers() { - ByteBuffer duplicate = this.byteBuffer.duplicate(); - duplicate.clear().position(this.writePosition).limit(this.capacity - this.writePosition); - return new ByteBufferIterator(duplicate); + ByteBuffer slice = this.byteBuffer.slice(this.writePosition, writableByteCount()); + return new ByteBufferIterator(slice); } @Override diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java index 62fa289e7c37..d089fc4ec965 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,7 +95,7 @@ private void updateCount(int bytesToAdd) { } private void raiseLimitException() { - // Do not release here, it's likely down via doOnDiscard.. + // Do not release here, it's likely done via doOnDiscard throw new DataBufferLimitException( "Exceeded limit on max bytes to buffer : " + this.maxByteCount); } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java b/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java new file mode 100644 index 000000000000..ef4eee362424 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java @@ -0,0 +1,348 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.io.buffer; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; +import java.util.function.Consumer; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.lang.Nullable; + +/** + * Bridges between {@link OutputStream} and + * {@link Publisher Publisher<DataBuffer>}. + * + *

    Note that this class has a near duplicate in + * {@link org.springframework.http.client.OutputStreamPublisher}. + * + * @author Oleh Dokuka + * @author Arjen Poutsma + * @since 6.1 + */ +final class OutputStreamPublisher implements Publisher { + + private final Consumer outputStreamConsumer; + + private final DataBufferFactory bufferFactory; + + private final Executor executor; + + private final int chunkSize; + + + OutputStreamPublisher(Consumer outputStreamConsumer, DataBufferFactory bufferFactory, + Executor executor, int chunkSize) { + + this.outputStreamConsumer = outputStreamConsumer; + this.bufferFactory = bufferFactory; + this.executor = executor; + this.chunkSize = chunkSize; + } + + + @Override + public void subscribe(Subscriber subscriber) { + Objects.requireNonNull(subscriber, "Subscriber must not be null"); + + OutputStreamSubscription subscription = new OutputStreamSubscription( + subscriber, this.outputStreamConsumer, this.bufferFactory, this.chunkSize); + + subscriber.onSubscribe(subscription); + this.executor.execute(subscription::invokeHandler); + } + + + private static final class OutputStreamSubscription extends OutputStream implements Subscription { + + private static final Object READY = new Object(); + + private final Subscriber actual; + + private final Consumer outputStreamHandler; + + private final DataBufferFactory bufferFactory; + + private final int chunkSize; + + private final AtomicLong requested = new AtomicLong(); + + private final AtomicReference parkedThread = new AtomicReference<>(); + + @Nullable + private volatile Throwable error; + + private long produced; + + OutputStreamSubscription(Subscriber actual, + Consumer outputStreamConsumer, DataBufferFactory bufferFactory, int chunkSize) { + + this.actual = actual; + this.outputStreamHandler = outputStreamConsumer; + this.bufferFactory = bufferFactory; + this.chunkSize = chunkSize; + } + + @Override + public void write(int b) throws IOException { + checkDemandAndAwaitIfNeeded(); + + DataBuffer next = this.bufferFactory.allocateBuffer(1); + next.write((byte) b); + + this.actual.onNext(next); + + this.produced++; + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + checkDemandAndAwaitIfNeeded(); + + DataBuffer next = this.bufferFactory.allocateBuffer(len); + next.write(b, off, len); + + this.actual.onNext(next); + + this.produced++; + } + + private void checkDemandAndAwaitIfNeeded() throws IOException { + long r = this.requested.get(); + + if (isTerminated(r) || isCancelled(r)) { + throw new IOException("Subscription has been terminated"); + } + + long p = this.produced; + if (p == r) { + if (p > 0) { + r = tryProduce(p); + this.produced = 0; + } + + while (true) { + if (isTerminated(r) || isCancelled(r)) { + throw new IOException("Subscription has been terminated"); + } + + if (r != 0) { + return; + } + + await(); + + r = this.requested.get(); + } + } + } + + private void invokeHandler() { + // assume sync write within try-with-resource block + + // use BufferedOutputStream, so that written bytes are buffered + // before publishing as byte buffer + try (OutputStream outputStream = new BufferedOutputStream(this, this.chunkSize)) { + this.outputStreamHandler.accept(outputStream); + } + catch (Exception ex) { + long previousState = tryTerminate(); + if (isCancelled(previousState)) { + return; + } + if (isTerminated(previousState)) { + // failure due to illegal requestN + Throwable error = this.error; + if (error != null) { + this.actual.onError(error); + return; + } + } + this.actual.onError(ex); + return; + } + + long previousState = tryTerminate(); + if (isCancelled(previousState)) { + return; + } + if (isTerminated(previousState)) { + // failure due to illegal requestN + Throwable error = this.error; + if (error != null) { + this.actual.onError(error); + return; + } + } + this.actual.onComplete(); + } + + + @Override + public void request(long n) { + if (n <= 0) { + this.error = new IllegalArgumentException("request should be a positive number"); + long previousState = tryTerminate(); + if (isTerminated(previousState) || isCancelled(previousState)) { + return; + } + if (previousState > 0) { + // error should eventually be observed and propagated + return; + } + // resume parked thread, so it can observe error and propagate it + resume(); + return; + } + + if (addCap(n) == 0) { + // resume parked thread so it can continue the work + resume(); + } + } + + @Override + public void cancel() { + long previousState = tryCancel(); + if (isCancelled(previousState) || previousState > 0) { + return; + } + + // resume parked thread, so it can be unblocked and close all the resources + resume(); + } + + private void await() { + Thread toUnpark = Thread.currentThread(); + + while (true) { + Object current = this.parkedThread.get(); + if (current == READY) { + break; + } + + if (current != null && current != toUnpark) { + throw new IllegalStateException("Only one (Virtual)Thread can await!"); + } + + if (this.parkedThread.compareAndSet(null, toUnpark)) { + LockSupport.park(); + // we don't just break here because park() can wake up spuriously + // if we got a proper resume, get() == READY and the loop will quit above + } + } + // clear the resume indicator so that the next await call will park without a resume() + this.parkedThread.lazySet(null); + } + + private void resume() { + if (this.parkedThread.get() != READY) { + Object old = this.parkedThread.getAndSet(READY); + if (old != READY) { + LockSupport.unpark((Thread)old); + } + } + } + + private long tryCancel() { + while (true) { + long r = this.requested.get(); + if (isCancelled(r)) { + return r; + } + if (this.requested.compareAndSet(r, Long.MIN_VALUE)) { + return r; + } + } + } + + private long tryTerminate() { + while (true) { + long r = this.requested.get(); + if (isCancelled(r) || isTerminated(r)) { + return r; + } + if (this.requested.compareAndSet(r, Long.MIN_VALUE | Long.MAX_VALUE)) { + return r; + } + } + } + + private long tryProduce(long n) { + while (true) { + long current = this.requested.get(); + if (isTerminated(current) || isCancelled(current)) { + return current; + } + if (current == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long update = current - n; + if (update < 0L) { + update = 0L; + } + if (this.requested.compareAndSet(current, update)) { + return update; + } + } + } + + private long addCap(long n) { + while (true) { + long r = this.requested.get(); + if (isTerminated(r) || isCancelled(r) || r == Long.MAX_VALUE) { + return r; + } + long u = addCap(r, n); + if (this.requested.compareAndSet(r, u)) { + return r; + } + } + } + + private static boolean isTerminated(long state) { + return state == (Long.MIN_VALUE | Long.MAX_VALUE); + } + + private static boolean isCancelled(long state) { + return state == Long.MIN_VALUE; + } + + private static long addCap(long a, long b) { + long res = a + b; + if (res < 0L) { + return Long.MAX_VALUE; + } + return res; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 67159494bdc7..2f68ab342974 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import java.net.URLConnection; import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystems; +import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; @@ -71,18 +72,18 @@ /** * A {@link ResourcePatternResolver} implementation that is able to resolve a * specified resource location path into one or more matching Resources. - * The source path may be a simple path which has a one-to-one mapping to a - * target {@link org.springframework.core.io.Resource}, or alternatively - * may contain the special "{@code classpath*:}" prefix and/or - * internal Ant-style regular expressions (matched using Spring's - * {@link org.springframework.util.AntPathMatcher} utility). - * Both of the latter are effectively wildcards. * - *

    No Wildcards: + *

    The source path may be a simple path which has a one-to-one mapping to a + * target {@link org.springframework.core.io.Resource}, or alternatively may + * contain the special "{@code classpath*:}" prefix and/or internal Ant-style + * path patterns (matched using Spring's {@link AntPathMatcher} utility). Both + * of the latter are effectively wildcards. + * + *

    No Wildcards

    * *

    In the simple case, if the specified location path does not start with the - * {@code "classpath*:}" prefix, and does not contain a PathMatcher pattern, - * this resolver will simply return a single resource via a + * {@code "classpath*:}" prefix and does not contain a {@link PathMatcher} + * pattern, this resolver will simply return a single resource via a * {@code getResource()} call on the underlying {@code ResourceLoader}. * Examples are real URLs such as "{@code file:C:/context.xml}", pseudo-URLs * such as "{@code classpath:/context.xml}", and simple unprefixed paths @@ -90,14 +91,14 @@ * fashion specific to the underlying {@code ResourceLoader} (e.g. * {@code ServletContextResource} for a {@code WebApplicationContext}). * - *

    Ant-style Patterns: + *

    Ant-style Patterns

    * - *

    When the path location contains an Ant-style pattern, e.g.: + *

    When the path location contains an Ant-style pattern, for example: *

      * /WEB-INF/*-context.xml
    - * com/mycompany/**/applicationContext.xml
    + * com/example/**/applicationContext.xml
      * file:C:/some/path/*-context.xml
    - * classpath:com/mycompany/**/applicationContext.xml
    + * classpath:com/example/**/applicationContext.xml * the resolver follows a more complex but defined procedure to try to resolve * the wildcard. It produces a {@code Resource} for the path up to the last * non-wildcard segment and obtains a {@code URL} from it. If this URL is not a @@ -108,31 +109,31 @@ * {@code java.net.JarURLConnection} from it, or manually parses the jar URL, and * then traverses the contents of the jar file, to resolve the wildcards. * - *

    Implications on portability: + *

    Implications on Portability

    * *

    If the specified path is already a file URL (either explicitly, or * implicitly because the base {@code ResourceLoader} is a filesystem one), * then wildcarding is guaranteed to work in a completely portable fashion. * - *

    If the specified path is a classpath location, then the resolver must + *

    If the specified path is a class path location, then the resolver must * obtain the last non-wildcard path segment URL via a * {@code Classloader.getResource()} call. Since this is just a * node of the path (not the file at the end) it is actually undefined * (in the ClassLoader Javadocs) exactly what sort of URL is returned in * this case. In practice, it is usually a {@code java.io.File} representing - * the directory, where the classpath resource resolves to a filesystem - * location, or a jar URL of some sort, where the classpath resource resolves + * the directory, where the class path resource resolves to a filesystem + * location, or a jar URL of some sort, where the class path resource resolves * to a jar location. Still, there is a portability concern on this operation. * *

    If a jar URL is obtained for the last non-wildcard segment, the resolver * must be able to get a {@code java.net.JarURLConnection} from it, or - * manually parse the jar URL, to be able to walk the contents of the jar, - * and resolve the wildcard. This will work in most environments, but will + * manually parse the jar URL, to be able to walk the contents of the jar + * and resolve the wildcard. This will work in most environments but will * fail in others, and it is strongly recommended that the wildcard * resolution of resources coming from jars be thoroughly tested in your * specific environment before you rely on it. * - *

    {@code classpath*:} Prefix: + *

    {@code classpath*:} Prefix

    * *

    There is special support for retrieving multiple class path resources with * the same name, via the "{@code classpath*:}" prefix. For example, @@ -142,22 +143,22 @@ * at the same location within each jar file. Internally, this happens via a * {@code ClassLoader.getResources()} call, and is completely portable. * - *

    The "classpath*:" prefix can also be combined with a PathMatcher pattern in - * the rest of the location path, for example "classpath*:META-INF/*-beans.xml". - * In this case, the resolution strategy is fairly simple: a - * {@code ClassLoader.getResources()} call is used on the last non-wildcard - * path segment to get all the matching resources in the class loader hierarchy, - * and then off each resource the same PathMatcher resolution strategy described - * above is used for the wildcard sub pattern. + *

    The "{@code classpath*:}" prefix can also be combined with a {@code PathMatcher} + * pattern in the rest of the location path — for example, + * "{@code classpath*:META-INF/*-beans.xml"}. In this case, the resolution strategy + * is fairly simple: a {@code ClassLoader.getResources()} call is used on the last + * non-wildcard path segment to get all the matching resources in the class loader + * hierarchy, and then off each resource the same {@code PathMatcher} resolution + * strategy described above is used for the wildcard sub pattern. * - *

    Other notes: + *

    Other Notes

    * - *

    As of Spring Framework 6.0, if {@link #getResources(String)} is invoked - * with a location pattern using the "classpath*:" prefix it will first search + *

    As of Spring Framework 6.0, if {@link #getResources(String)} is invoked with + * a location pattern using the "{@code classpath*:}" prefix it will first search * all modules in the {@linkplain ModuleLayer#boot() boot layer}, excluding * {@linkplain ModuleFinder#ofSystem() system modules}. It will then search the - * classpath using {@link ClassLoader} APIs as described previously and return the - * combined results. Consequently, some of the limitations of classpath searches + * class path using {@link ClassLoader} APIs as described previously and return the + * combined results. Consequently, some of the limitations of class path searches * may not apply when applications are deployed as modules. * *

    WARNING: Note that "{@code classpath*:}" when combined with @@ -168,26 +169,26 @@ * root of expanded directories. This originates from a limitation in the JDK's * {@code ClassLoader.getResources()} method which only returns file system * locations for a passed-in empty String (indicating potential roots to search). - * This {@code ResourcePatternResolver} implementation is trying to mitigate the + * This {@code ResourcePatternResolver} implementation tries to mitigate the * jar root lookup limitation through {@link URLClassLoader} introspection and - * "java.class.path" manifest evaluation; however, without portability guarantees. + * "{@code java.class.path}" manifest evaluation; however, without portability + * guarantees. * - *

    WARNING: Ant-style patterns with "classpath:" resources are not - * guaranteed to find matching resources if the root package to search is available + *

    WARNING: Ant-style patterns with "{@code classpath:}" resources are not + * guaranteed to find matching resources if the base package to search is available * in multiple class path locations. This is because a resource such as *

    - *     com/mycompany/package1/service-context.xml
    - * 
    - * may be in only one location, but when a path such as + * com/example/package1/service-context.xml + * may exist in only one class path location, but when a location pattern such as *
    - *     classpath:com/mycompany/**/service-context.xml
    - * 
    + * classpath:com/example/**/service-context.xml * is used to try to resolve it, the resolver will work off the (first) URL - * returned by {@code getResource("com/mycompany");}. If this base package node - * exists in multiple classloader locations, the actual end resource may not be - * underneath. Therefore, preferably, use "{@code classpath*:}" with the same - * Ant-style pattern in such a case, which will search all class path - * locations that contain the root package. + * returned by {@code getResource("com/example")}. If the {@code com/example} base + * package node exists in multiple class path locations, the actual desired resource + * may not be present under the {@code com/example} base package in the first URL. + * Therefore, preferably, use "{@code classpath*:}" with the same Ant-style pattern + * in such a case, which will search all class path locations that contain + * the base package. * * @author Juergen Hoeller * @author Colin Sampaleanu @@ -249,19 +250,21 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol /** - * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. + * Create a {@code PathMatchingResourcePatternResolver} with a + * {@link DefaultResourceLoader}. *

    ClassLoader access will happen via the thread context class loader. - * @see org.springframework.core.io.DefaultResourceLoader + * @see DefaultResourceLoader */ public PathMatchingResourcePatternResolver() { this.resourceLoader = new DefaultResourceLoader(); } /** - * Create a new PathMatchingResourcePatternResolver. + * Create a {@code PathMatchingResourcePatternResolver} with the supplied + * {@link ResourceLoader}. *

    ClassLoader access will happen via the thread context class loader. - * @param resourceLoader the ResourceLoader to load root directories and - * actual resources with + * @param resourceLoader the {@code ResourceLoader} to load root directories + * and actual resources with */ public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { Assert.notNull(resourceLoader, "ResourceLoader must not be null"); @@ -269,8 +272,9 @@ public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { } /** - * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. - * @param classLoader the ClassLoader to load classpath resources with, + * Create a {@code PathMatchingResourcePatternResolver} with a + * {@link DefaultResourceLoader} and the supplied {@link ClassLoader}. + * @param classLoader the ClassLoader to load class path resources with, * or {@code null} for using the thread context class loader * at the time of actual resource access * @see org.springframework.core.io.DefaultResourceLoader @@ -281,7 +285,7 @@ public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) { /** - * Return the ResourceLoader that this pattern resolver works with. + * Return the {@link ResourceLoader} that this pattern resolver works with. */ public ResourceLoader getResourceLoader() { return this.resourceLoader; @@ -294,9 +298,10 @@ public ClassLoader getClassLoader() { } /** - * Set the PathMatcher implementation to use for this - * resource pattern resolver. Default is AntPathMatcher. - * @see org.springframework.util.AntPathMatcher + * Set the {@link PathMatcher} implementation to use for this + * resource pattern resolver. + *

    Default is {@link AntPathMatcher}. + * @see AntPathMatcher */ public void setPathMatcher(PathMatcher pathMatcher) { Assert.notNull(pathMatcher, "PathMatcher must not be null"); @@ -304,7 +309,7 @@ public void setPathMatcher(PathMatcher pathMatcher) { } /** - * Return the PathMatcher that this resource pattern resolver uses. + * Return the {@link PathMatcher} that this resource pattern resolver uses. */ public PathMatcher getPathMatcher() { return this.pathMatcher; @@ -353,8 +358,8 @@ public Resource[] getResources(String locationPattern) throws IOException { /** * Find all class location resources with the given location via the ClassLoader. - * Delegates to {@link #doFindAllClassPathResources(String)}. - * @param location the absolute path within the classpath + *

    Delegates to {@link #doFindAllClassPathResources(String)}. + * @param location the absolute path within the class path * @return the result as Resource array * @throws IOException in case of I/O errors * @see java.lang.ClassLoader#getResources @@ -364,15 +369,16 @@ protected Resource[] findAllClassPathResources(String location) throws IOExcepti String path = stripLeadingSlash(location); Set result = doFindAllClassPathResources(path); if (logger.isTraceEnabled()) { - logger.trace("Resolved classpath location [" + path + "] to resources " + result); + logger.trace("Resolved class path location [" + path + "] to resources " + result); } return result.toArray(new Resource[0]); } /** - * Find all class location resources with the given path via the ClassLoader. - * Called by {@link #findAllClassPathResources(String)}. - * @param path the absolute path within the classpath (never a leading slash) + * Find all class path resources with the given path via the configured + * {@link #getClassLoader() ClassLoader}. + *

    Called by {@link #findAllClassPathResources(String)}. + * @param path the absolute path within the class path (never a leading slash) * @return a mutable Set of matching Resource instances * @since 4.1.1 */ @@ -386,20 +392,21 @@ protected Set doFindAllClassPathResources(String path) throws IOExcept } if (!StringUtils.hasLength(path)) { // The above result is likely to be incomplete, i.e. only containing file system references. - // We need to have pointers to each of the jar files on the classpath as well... + // We need to have pointers to each of the jar files on the class path as well... addAllClassLoaderJarRoots(cl, result); } return result; } /** - * Convert the given URL as returned from the ClassLoader into a {@link Resource}, - * applying to path lookups without a pattern ({@link #findAllClassPathResources}). + * Convert the given URL as returned from the configured + * {@link #getClassLoader() ClassLoader} into a {@link Resource}, applying + * to path lookups without a pattern (see {@link #findAllClassPathResources}). *

    As of 6.0.5, the default implementation creates a {@link FileSystemResource} * in case of the "file" protocol or a {@link UrlResource} otherwise, matching - * the outcome of pattern-based classpath traversal in the same resource layout, + * the outcome of pattern-based class path traversal in the same resource layout, * as well as matching the outcome of module path searches. - * @param url a URL as returned from the ClassLoader + * @param url a URL as returned from the configured ClassLoader * @return the corresponding Resource object * @see java.lang.ClassLoader#getResources * @see #doFindAllClassPathResources @@ -417,13 +424,30 @@ protected Resource convertClassLoaderURL(URL url) { } } else { + String urlString = url.toString(); + String cleanedPath = StringUtils.cleanPath(urlString); + if (!cleanedPath.equals(urlString)) { + // Prefer cleaned URL, aligned with UrlResource#createRelative(String) + try { + // Cannot test for URLStreamHandler directly: URL equality for same String + // in order to find out whether original URL uses default URLStreamHandler. + if (ResourceUtils.toURL(urlString).equals(url)) { + // Plain URL with default URLStreamHandler -> replace with cleaned path. + return new UrlResource(ResourceUtils.toURI(cleanedPath)); + } + } + catch (URISyntaxException | MalformedURLException ex) { + // Fallback to regular URL construction below... + } + } + // Retain original URL instance, potentially including custom URLStreamHandler. return new UrlResource(url); } } /** - * Search all {@link URLClassLoader} URLs for jar file references and add them to the - * given set of resources in the form of pointers to the root of the jar file content. + * Search all {@link URLClassLoader} URLs for jar file references and add each to the + * given set of resources in the form of a pointer to the root of the jar file content. * @param classLoader the ClassLoader to search (including its ancestors) * @param result the set of resources to add jar roots to * @since 4.1.1 @@ -457,7 +481,7 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set< } if (classLoader == ClassLoader.getSystemClassLoader()) { - // "java.class.path" manifest evaluation... + // JAR "Class-Path" manifest header evaluation... addClassPathManifestEntries(result); } @@ -476,16 +500,17 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set< } /** - * Determine jar file references from the "java.class.path." manifest property and add them - * to the given set of resources in the form of pointers to the root of the jar file content. + * Determine jar file references from {@code Class-Path} manifest entries (which + * are added to the {@code java.class.path} JVM system property by the system + * class loader) and add each to the given set of resources in the form of + * a pointer to the root of the jar file content. * @param result the set of resources to add jar roots to * @since 4.3 */ protected void addClassPathManifestEntries(Set result) { try { String javaClassPathProperty = System.getProperty("java.class.path"); - for (String path : StringUtils.delimitedListToStringArray( - javaClassPathProperty, System.getProperty("path.separator"))) { + for (String path : StringUtils.delimitedListToStringArray(javaClassPathProperty, File.pathSeparator)) { try { String filePath = new File(path).getAbsolutePath(); int prefixIndex = filePath.indexOf(':'); @@ -499,7 +524,7 @@ protected void addClassPathManifestEntries(Set result) { // Build URL that points to the root of the jar file UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + filePath + ResourceUtils.JAR_URL_SEPARATOR); - // Potentially overlapping with URLClassLoader.getURLs() result above! + // Potentially overlapping with URLClassLoader.getURLs() result in addAllClassLoaderJarRoots(). if (!result.contains(jarResource) && !hasDuplicate(filePath, result) && jarResource.exists()) { result.add(jarResource); } @@ -543,21 +568,25 @@ private boolean hasDuplicate(String filePath, Set result) { } /** - * Find all resources that match the given location pattern via the - * Ant-style PathMatcher. Supports resources in OSGi bundles, JBoss VFS, - * jar files, zip files, and file systems. + * Find all resources that match the given location pattern via the Ant-style + * {@link #getPathMatcher() PathMatcher}. + *

    Supports resources in OSGi bundles, JBoss VFS, jar files, zip files, + * and file systems. * @param locationPattern the location pattern to match * @return the result as Resource array * @throws IOException in case of I/O errors - * @see #doFindPathMatchingJarResources - * @see #doFindPathMatchingFileResources + * @see #determineRootDir(String) + * @see #resolveRootDirResource(Resource) + * @see #isJarResource(Resource) + * @see #doFindPathMatchingJarResources(Resource, URL, String) + * @see #doFindPathMatchingFileResources(Resource, String) * @see org.springframework.util.PathMatcher */ protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { String rootDirPath = determineRootDir(locationPattern); String subPattern = locationPattern.substring(rootDirPath.length()); Resource[] rootDirResources = getResources(rootDirPath); - Set result = new LinkedHashSet<>(16); + Set result = new LinkedHashSet<>(64); for (Resource rootDirResource : rootDirResources) { rootDirResource = resolveRootDirResource(rootDirResource); URL rootDirUrl = rootDirResource.getURL(); @@ -607,29 +636,39 @@ protected String determineRootDir(String location) { } /** - * Resolve the specified resource for path matching. - *

    By default, Equinox OSGi "bundleresource:" / "bundleentry:" URL will be - * resolved into a standard jar file URL that be traversed using Spring's - * standard jar file traversal algorithm. For any preceding custom resolution, - * override this method and replace the resource handle accordingly. + * Resolve the supplied root directory resource for path matching. + *

    By default, {@link #findPathMatchingResources(String)} resolves Equinox + * OSGi "bundleresource:" and "bundleentry:" URLs into standard jar file URLs + * that will be traversed using Spring's standard jar file traversal algorithm. + *

    For any custom resolution, override this template method and replace the + * supplied resource handle accordingly. + *

    The default implementation of this method returns the supplied resource + * unmodified. * @param original the resource to resolve - * @return the resolved resource (may be identical to the passed-in resource) + * @return the resolved resource (may be identical to the supplied resource) * @throws IOException in case of resolution failure + * @see #findPathMatchingResources(String) */ protected Resource resolveRootDirResource(Resource original) throws IOException { return original; } /** - * Return whether the given resource handle indicates a jar resource - * that the {@link #doFindPathMatchingJarResources} method can handle. - *

    By default, the URL protocols "jar", "zip", "vfszip, and "wsjar" - * will be treated as jar resources. This template method allows for - * detecting further kinds of jar-like resources, e.g. through - * {@code instanceof} checks on the resource handle type. - * @param resource the resource handle to check - * (usually the root directory to start path matching from) - * @see #doFindPathMatchingJarResources + * Determine if the given resource handle indicates a jar resource that the + * {@link #doFindPathMatchingJarResources} method can handle. + *

    {@link #findPathMatchingResources(String)} delegates to + * {@link ResourceUtils#isJarURL(URL)} to determine whether the given URL + * points to a resource in a jar file, and only invokes this method as a fallback. + *

    This template method therefore allows for detecting further kinds of + * jar-like resources — for example, via {@code instanceof} checks on + * the resource handle type. + *

    The default implementation of this method returns {@code false}. + * @param resource the resource handle to check (usually the root directory + * to start path matching from) + * @return {@code true} if the given resource handle indicates a jar resource + * @throws IOException in case of I/O errors + * @see #findPathMatchingResources(String) + * @see #doFindPathMatchingJarResources(Resource, URL, String) * @see org.springframework.util.ResourceUtils#isJarURL */ protected boolean isJarResource(Resource resource) throws IOException { @@ -638,7 +677,7 @@ protected boolean isJarResource(Resource resource) throws IOException { /** * Find all resources in jar files that match the given location pattern - * via the Ant-style PathMatcher. + * via the Ant-style {@link #getPathMatcher() PathMatcher}. * @param rootDirResource the root directory as Resource * @param rootDirUrl the pre-resolved root directory URL * @param subPattern the sub pattern to match (below the root directory) @@ -659,7 +698,6 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, if (con instanceof JarURLConnection jarCon) { // Should usually be the case for traditional JAR files. - ResourceUtils.useCachesIfNecessary(jarCon); jarFile = jarCon.getJarFile(); jarFileUrl = jarCon.getJarFileURL().toExternalForm(); JarEntry jarEntry = jarCon.getJarEntry(); @@ -691,7 +729,7 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, } catch (ZipException ex) { if (logger.isDebugEnabled()) { - logger.debug("Skipping invalid jar classpath entry [" + urlFile + "]"); + logger.debug("Skipping invalid jar class path entry [" + urlFile + "]"); } return Collections.emptySet(); } @@ -706,7 +744,7 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, // The Sun JRE does not return a slash here, but BEA JRockit does. rootEntryPath = rootEntryPath + "/"; } - Set result = new LinkedHashSet<>(8); + Set result = new LinkedHashSet<>(64); for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) { JarEntry entry = entries.nextElement(); String entryPath = entry.getName(); @@ -746,7 +784,8 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException { /** * Find all resources in the file system of the supplied root directory that - * match the given location sub pattern via the Ant-style PathMatcher. + * match the given location sub pattern via the Ant-style {@link #getPathMatcher() + * PathMatcher}. * @param rootDirResource the root directory as a Resource * @param subPattern the sub pattern to match (below the root directory) * @return a mutable Set of matching Resource instances @@ -756,7 +795,7 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException { protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { - Set result = new LinkedHashSet<>(); + Set result = new LinkedHashSet<>(64); URI rootDirUri; try { rootDirUri = rootDirResource.getURI(); @@ -833,7 +872,7 @@ protected Set doFindPathMatchingFileResources(Resource rootDirResource .formatted(rootPath.toAbsolutePath(), subPattern)); } - try (Stream files = Files.walk(rootPath)) { + try (Stream files = Files.walk(rootPath, FileVisitOption.FOLLOW_LINKS)) { files.filter(isMatchingFile).sorted().map(FileSystemResource::new).forEach(result::add); } catch (Exception ex) { @@ -865,7 +904,7 @@ protected Set doFindPathMatchingFileResources(Resource rootDirResource * @see PathMatcher#match(String, String) */ protected Set findAllModulePathResources(String locationPattern) throws IOException { - Set result = new LinkedHashSet<>(16); + Set result = new LinkedHashSet<>(64); // Skip scanning the module path when running in a native image. if (NativeDetector.inNativeImage()) { @@ -924,8 +963,8 @@ private Resource findResource(ModuleReader moduleReader, String name) { } /** - * If it's a "file:" URI, use FileSystemResource to avoid duplicates - * for the same path discovered via class-path scanning. + * If it's a "file:" URI, use {@link FileSystemResource} to avoid duplicates + * for the same path discovered via class path scanning. */ private Resource convertModuleSystemURI(URI uri) { return (ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()) ? @@ -966,7 +1005,7 @@ private static class PatternVirtualFileVisitor implements InvocationHandler { private final String rootPath; - private final Set resources = new LinkedHashSet<>(); + private final Set resources = new LinkedHashSet<>(64); public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) { this.subPattern = subPattern; @@ -979,26 +1018,25 @@ public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (Object.class == method.getDeclaringClass()) { - if (methodName.equals("equals")) { - // Only consider equal when proxies are identical. - return (proxy == args[0]); - } - else if (methodName.equals("hashCode")) { - return System.identityHashCode(proxy); + switch (methodName) { + case "equals" -> { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + case "hashCode" -> { + return System.identityHashCode(proxy); + } } } - else if ("getAttributes".equals(methodName)) { - return getAttributes(); - } - else if ("visit".equals(methodName)) { - visit(args[0]); - return null; - } - else if ("toString".equals(methodName)) { - return toString(); - } - - throw new IllegalStateException("Unexpected method invocation: " + method); + return switch (methodName) { + case "getAttributes" -> getAttributes(); + case "visit" -> { + visit(args[0]); + yield null; + } + case "toString" -> toString(); + default -> throw new IllegalStateException("Unexpected method invocation: " + method); + }; } public void visit(Object vfsResource) { diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java index 254a1d49d68c..c138de880488 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java @@ -24,6 +24,8 @@ /** * Descriptor for a {@link org.springframework.core.env.PropertySource PropertySource}. * + * @author Stephane Nicoll + * @since 6.0 * @param locations the locations to consider * @param ignoreResourceNotFound whether a failure to find a property resource * should be ignored @@ -31,8 +33,6 @@ * @param propertySourceFactory the type of {@link PropertySourceFactory} to use, * or {@code null} to use the default * @param encoding the encoding, or {@code null} to use the default encoding - * @author Stephane Nicoll - * @since 6.0 * @see org.springframework.core.env.PropertySource * @see org.springframework.context.annotation.PropertySource */ diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java index 21cba7596cb8..1f62a19c1d3f 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,11 +27,20 @@ * @author Juergen Hoeller * @since 4.3 * @see DefaultPropertySourceFactory + * @see ResourcePropertySource */ public interface PropertySourceFactory { /** * Create a {@link PropertySource} that wraps the given resource. + *

    Implementations will typically create {@link ResourcePropertySource} + * instances, with {@link PropertySourceProcessor} automatically adapting + * property source names via {@link ResourcePropertySource#withResourceName()} + * if necessary, e.g. when combining multiple sources for the same name + * into a {@link org.springframework.core.env.CompositePropertySource}. + * Custom implementations with custom {@link PropertySource} types need + * to make sure to expose distinct enough names, possibly deriving from + * {@link ResourcePropertySource} where possible. * @param name the name of the property source * (can be {@code null} in which case the factory implementation * will have to generate a name based on the given resource) diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java index 407c5019e71d..7559ce88641d 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java @@ -46,6 +46,7 @@ * * @author Stephane Nicoll * @author Sam Brannen + * @author Juergen Hoeller * @since 6.0 * @see PropertySourceDescriptor */ @@ -58,14 +59,14 @@ public class PropertySourceProcessor { private final ConfigurableEnvironment environment; - private final ResourceLoader resourceLoader; + private final ResourcePatternResolver resourcePatternResolver; private final List propertySourceNames = new ArrayList<>(); public PropertySourceProcessor(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { this.environment = environment; - this.resourceLoader = resourceLoader; + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); } @@ -87,8 +88,9 @@ public void processPropertySource(PropertySourceDescriptor descriptor) throws IO for (String location : locations) { try { String resolvedLocation = this.environment.resolveRequiredPlaceholders(location); - Resource resource = this.resourceLoader.getResource(resolvedLocation); - addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding))); + for (Resource resource : this.resourcePatternResolver.getResources(resolvedLocation)) { + addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding))); + } } catch (RuntimeException | IOException ex) { // Placeholders not resolvable (IllegalArgumentException) or resource not found when trying to open it @@ -135,8 +137,8 @@ private void addPropertySource(PropertySource propertySource) { propertySources.addLast(propertySource); } else { - String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1); - propertySources.addBefore(firstProcessed, propertySource); + String lastAdded = this.propertySourceNames.get(this.propertySourceNames.size() - 1); + propertySources.addBefore(lastAdded, propertySource); } this.propertySourceNames.add(name); } diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java index 0bc2a6612352..f54fc1f5261c 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Editor for {@link org.springframework.core.io.Resource} arrays, to @@ -50,6 +51,8 @@ * * @author Juergen Hoeller * @author Chris Beams + * @author Yanming Zhou + * @author Stephane Nicoll * @since 1.1.2 * @see org.springframework.core.io.Resource * @see ResourcePatternResolver @@ -108,17 +111,30 @@ public ResourceArrayPropertyEditor(ResourcePatternResolver resourcePatternResolv /** - * Treat the given text as a location pattern and convert it to a Resource array. + * Treat the given text as a location pattern or comma delimited location patterns + * and convert it to a Resource array. */ @Override public void setAsText(String text) { String pattern = resolvePath(text).trim(); + String[] locationPatterns = StringUtils.commaDelimitedListToStringArray(pattern); + if (locationPatterns.length == 1) { + setValue(getResources(locationPatterns[0])); + } + else { + Resource[] resources = Arrays.stream(locationPatterns).map(String::trim) + .map(this::getResources).flatMap(Arrays::stream).toArray(Resource[]::new); + setValue(resources); + } + } + + private Resource[] getResources(String locationPattern) { try { - setValue(this.resourcePatternResolver.getResources(pattern)); + return this.resourcePatternResolver.getResources(locationPattern); } catch (IOException ex) { throw new IllegalArgumentException( - "Could not resolve resource location pattern [" + pattern + "]: " + ex.getMessage()); + "Could not resolve resource location pattern [" + locationPattern + "]: " + ex.getMessage()); } } diff --git a/spring-core/src/main/java/org/springframework/core/log/LogMessage.java b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java index 168cc3969e2f..e1206d5c3530 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogMessage.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java @@ -163,7 +163,7 @@ String buildString() { } - private static abstract class FormatMessage extends LogMessage { + private abstract static class FormatMessage extends LogMessage { protected final String format; diff --git a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java index 9fdb24ac9ed3..9a497d3eb9ee 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.util.Iterator; import java.util.function.Supplier; +import org.springframework.lang.Nullable; + /** * Default "no op" {@code ApplicationStartup} implementation. * @@ -52,6 +54,7 @@ public long getId() { } @Override + @Nullable public Long getParentId() { return null; } @@ -73,7 +76,6 @@ public StartupStep tag(String key, Supplier value) { @Override public void end() { - } diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java index 915a52833c9d..c3d56b757d1e 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author 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.function.Supplier; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.NonNull; /** * {@link StartupStep} implementation for the Java Flight Recorder. + * *

    This variant delegates to a {@link FlightRecorderStartupEvent JFR event extension} * to collect and record data in Java Flight Recorder. * @@ -114,12 +114,12 @@ public void add(String key, Supplier value) { add(key, value.get()); } - @NonNull @Override public Iterator iterator() { return new TagsIterator(); } + private class TagsIterator implements Iterator { private int idx = 0; diff --git a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java index 07bf246b0581..3096daf9611f 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java @@ -65,7 +65,6 @@ public DefaultDeserializer(@Nullable ClassLoader classLoader) { * @see ObjectInputStream#readObject() */ @Override - @SuppressWarnings("resource") public Object deserialize(InputStream inputStream) throws IOException { ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader); try { diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java index 1f0fa8f17aed..bcae56cdc939 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,7 @@ public SerializingConverter(Serializer serializer) { */ @Override public byte[] convert(Object source) { - try { + try { return this.serializer.serializeToByteArray(source); } catch (Throwable ex) { diff --git a/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java index 126a58b2e0fb..3dd8891646b8 100644 --- a/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java +++ b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -178,14 +178,14 @@ protected String styleCollection(Collection collection) { */ protected String styleArray(Object[] array) { if (array.length == 0) { - return ARRAY + '<' + ClassUtils.getShortName(array.getClass().getComponentType()) + '>' + EMPTY; + return ARRAY + '<' + ClassUtils.getShortName(array.getClass().componentType()) + '>' + EMPTY; } StringJoiner result = new StringJoiner(", ", "[", "]"); for (Object element : array) { result.add(style(element)); } - return ARRAY + '<' + ClassUtils.getShortName(array.getClass().getComponentType()) + '>' + result; + return ARRAY + '<' + ClassUtils.getShortName(array.getClass().componentType()) + '>' + result; } /** diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java index 4d65c37313e4..39cc0b5cb651 100644 --- a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; import org.springframework.util.concurrent.FutureUtils; @@ -60,6 +61,8 @@ public interface AsyncTaskExecutor extends TaskExecutor { /** * Execute the given {@code task}. + *

    As of 6.1, this method comes with a default implementation that simply + * delegates to {@link #execute(Runnable)}, ignoring the timeout completely. * @param task the {@code Runnable} to execute (never {@code null}) * @param startTimeout the time duration (milliseconds) within which the task is * supposed to start. This is intended as a hint to the executor, allowing for @@ -72,27 +75,41 @@ public interface AsyncTaskExecutor extends TaskExecutor { * @deprecated as of 5.3.16 since the common executors do not support start timeouts */ @Deprecated - void execute(Runnable task, long startTimeout); + default void execute(Runnable task, long startTimeout) { + execute(task); + } /** * Submit a Runnable task for execution, receiving a Future representing that task. * The Future will return a {@code null} result upon completion. + *

    As of 6.1, this method comes with a default implementation that delegates + * to {@link #execute(Runnable)}. * @param task the {@code Runnable} to execute (never {@code null}) * @return a Future representing pending completion of the task * @throws TaskRejectedException if the given task was not accepted * @since 3.0 */ - Future submit(Runnable task); + default Future submit(Runnable task) { + FutureTask future = new FutureTask<>(task, null); + execute(future); + return future; + } /** * Submit a Callable task for execution, receiving a Future representing that task. * The Future will return the Callable's result upon completion. + *

    As of 6.1, this method comes with a default implementation that delegates + * to {@link #execute(Runnable)}. * @param task the {@code Callable} to execute (never {@code null}) * @return a Future representing pending completion of the task * @throws TaskRejectedException if the given task was not accepted * @since 3.0 */ - Future submit(Callable task); + default Future submit(Callable task) { + FutureTask future = new FutureTask<>(task); + execute(future, TIMEOUT_INDEFINITE); + return future; + } /** * Submit a {@code Runnable} task for execution, receiving a {@code CompletableFuture} diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 6f269e955046..4206695f0b42 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ package org.springframework.core.task; import java.io.Serializable; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.ThreadFactory; @@ -31,24 +34,29 @@ /** * {@link TaskExecutor} implementation that fires up a new Thread for each task, - * executing it asynchronously. + * executing it asynchronously. Provides a virtual thread option on JDK 21. * - *

    Supports limiting concurrent threads through {@link #setConcurrencyLimit}. + *

    Supports a graceful shutdown through {@link #setTaskTerminationTimeout}, + * at the expense of task tracking overhead per execution thread at runtime. + * Supports limiting concurrent threads through {@link #setConcurrencyLimit}. * By default, the number of concurrent task executions is unlimited. * *

    NOTE: This implementation does not reuse threads! Consider a * thread-pooling TaskExecutor implementation instead, in particular for - * executing a large number of short-lived tasks. + * executing a large number of short-lived tasks. Alternatively, on JDK 21, + * consider setting {@link #setVirtualThreads} to {@code true}. * * @author Juergen Hoeller * @since 2.0 + * @see #setVirtualThreads + * @see #setTaskTerminationTimeout * @see #setConcurrencyLimit - * @see SyncTaskExecutor + * @see org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor */ @SuppressWarnings({"serial", "deprecation"}) public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator - implements AsyncListenableTaskExecutor, Serializable { + implements AsyncListenableTaskExecutor, Serializable, AutoCloseable { /** * Permit any number of concurrent invocations: that is, don't throttle concurrency. @@ -66,12 +74,22 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator /** Internal concurrency throttle used by this executor. */ private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter(); + @Nullable + private VirtualThreadDelegate virtualThreadDelegate; + @Nullable private ThreadFactory threadFactory; @Nullable private TaskDecorator taskDecorator; + private long taskTerminationTimeout; + + @Nullable + private Set activeThreads; + + private volatile boolean active = true; + /** * Create a new SimpleAsyncTaskExecutor with default thread name prefix. @@ -97,6 +115,16 @@ public SimpleAsyncTaskExecutor(ThreadFactory threadFactory) { } + /** + * Switch this executor to virtual threads. Requires Java 21 or higher. + *

    The default is {@code false}, indicating platform threads. + * Set this flag to {@code true} in order to create virtual threads instead. + * @since 6.1 + */ + public void setVirtualThreads(boolean virtual) { + this.virtualThreadDelegate = (virtual ? new VirtualThreadDelegate() : null); + } + /** * Specify an external factory to use for creating new Threads, * instead of relying on the local properties of this executor. @@ -136,6 +164,28 @@ public void setTaskDecorator(TaskDecorator taskDecorator) { this.taskDecorator = taskDecorator; } + /** + * Specify a timeout (in milliseconds) for task termination when closing + * this executor. The default is 0, not waiting for task termination at all. + *

    Note that a concrete >0 timeout specified here will lead to the + * wrapping of every submitted task into a task-tracking runnable which + * involves considerable overhead in case of a high number of tasks. + * However, for a modest level of submissions with longer-running + * tasks, this is feasible in order to arrive at a graceful shutdown. + *

    Note that {@code SimpleAsyncTaskExecutor} does not participate in + * a coordinated lifecycle stop but rather just awaits task termination + * on {@link #close()}. + * @param timeout the timeout in milliseconds + * @since 6.1 + * @see #close() + * @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setAwaitTerminationMillis + */ + public void setTaskTerminationTimeout(long timeout) { + Assert.isTrue(timeout >= 0, "Timeout value must be >=0"); + this.taskTerminationTimeout = timeout; + this.activeThreads = (timeout > 0 ? Collections.newSetFromMap(new ConcurrentHashMap<>()) : null); + } + /** * Set the maximum number of parallel task executions allowed. * The default of -1 indicates no concurrency limit at all. @@ -165,6 +215,18 @@ public final boolean isThrottleActive() { return this.concurrencyThrottle.isThrottleActive(); } + /** + * Return whether this executor is still active, i.e. not closed yet, + * and therefore accepts further task submissions. Otherwise, it is + * either in the task termination phase or entirely shut down already. + * @since 6.1 + * @see #setTaskTerminationTimeout + * @see #close() + */ + public boolean isActive() { + return this.active; + } + /** * Executes the given task, within a concurrency throttle @@ -190,10 +252,17 @@ public void execute(Runnable task) { @Override public void execute(Runnable task, long startTimeout) { Assert.notNull(task, "Runnable must not be null"); + if (!isActive()) { + throw new TaskRejectedException(getClass().getSimpleName() + " has been closed already"); + } + Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task); if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) { this.concurrencyThrottle.beforeAccess(); - doExecute(new ConcurrencyThrottlingRunnable(taskToUse)); + doExecute(new TaskTrackingRunnable(taskToUse)); + } + else if (this.activeThreads != null) { + doExecute(new TaskTrackingRunnable(taskToUse)); } else { doExecute(taskToUse); @@ -236,13 +305,56 @@ public ListenableFuture submitListenable(Callable task) { * Template method for the actual execution of a task. *

    The default implementation creates a new Thread and starts it. * @param task the Runnable to execute + * @see #newThread + * @see Thread#start() + */ + protected void doExecute(Runnable task) { + newThread(task).start(); + } + + /** + * Create a new Thread for the given task. + * @param task the Runnable to create a Thread for + * @return the new Thread instance + * @since 6.1 + * @see #setVirtualThreads * @see #setThreadFactory * @see #createThread - * @see java.lang.Thread#start() */ - protected void doExecute(Runnable task) { - Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); - thread.start(); + protected Thread newThread(Runnable task) { + if (this.virtualThreadDelegate != null) { + return this.virtualThreadDelegate.newVirtualThread(nextThreadName(), task); + } + else { + return (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); + } + } + + /** + * This close methods tracks the termination of active threads if a concrete + * {@link #setTaskTerminationTimeout task termination timeout} has been set. + * Otherwise, it is not necessary to close this executor. + * @since 6.1 + */ + @Override + public void close() { + if (this.active) { + this.active = false; + Set threads = this.activeThreads; + if (threads != null) { + threads.forEach(Thread::interrupt); + synchronized (threads) { + try { + if (!threads.isEmpty()) { + threads.wait(this.taskTerminationTimeout); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } } @@ -266,23 +378,40 @@ protected void afterAccess() { /** - * This Runnable calls {@code afterAccess()} after the - * target Runnable has finished its execution. + * Decorates a target task with active thread tracking + * and concurrency throttle management, if necessary. */ - private class ConcurrencyThrottlingRunnable implements Runnable { + private class TaskTrackingRunnable implements Runnable { - private final Runnable target; + private final Runnable task; - public ConcurrencyThrottlingRunnable(Runnable target) { - this.target = target; + public TaskTrackingRunnable(Runnable task) { + Assert.notNull(task, "Task must not be null"); + this.task = task; } @Override public void run() { + Set threads = activeThreads; + Thread thread = null; + if (threads != null) { + thread = Thread.currentThread(); + threads.add(thread); + } try { - this.target.run(); + this.task.run(); } finally { + if (threads != null) { + threads.remove(thread); + if (!isActive()) { + synchronized (threads) { + if (threads.isEmpty()) { + threads.notify(); + } + } + } + } concurrencyThrottle.afterAccess(); } } diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java index e77d4df1bc8c..6d70d26f4bef 100644 --- a/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java +++ b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.core.task; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; /** @@ -50,4 +52,26 @@ public TaskRejectedException(String msg, Throwable cause) { super(msg, cause); } + /** + * Create a new {@code TaskRejectedException} + * with a default message for the given executor and task. + * @param executor the {@code Executor} that rejected the task + * @param task the task object that got rejected + * @param cause the original {@link RejectedExecutionException} + * @since 6.1 + * @see ExecutorService#isShutdown() + * @see java.util.concurrent.RejectedExecutionException + */ + public TaskRejectedException(Executor executor, Object task, RejectedExecutionException cause) { + super(executorDescription(executor) + " did not accept task: " + task, cause); + } + + + private static String executorDescription(Executor executor) { + if (executor instanceof ExecutorService executorService) { + return "ExecutorService in " + (executorService.isShutdown() ? "shutdown" : "active") + " state"; + } + return executor.toString(); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/task/VirtualThreadDelegate.java b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadDelegate.java new file mode 100644 index 000000000000..6f32b88e6122 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadDelegate.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task; + +import java.util.concurrent.ThreadFactory; + +/** + * Internal delegate for virtual thread handling on JDK 21. + * This is a dummy version for reachability on JDK <21. + * + * @author Juergen Hoeller + * @since 6.1 + * @see VirtualThreadTaskExecutor + */ +final class VirtualThreadDelegate { + + public VirtualThreadDelegate() { + throw new UnsupportedOperationException("Virtual threads not supported on JDK <21"); + } + + public ThreadFactory virtualThreadFactory() { + throw new UnsupportedOperationException(); + } + + public ThreadFactory virtualThreadFactory(String threadNamePrefix) { + throw new UnsupportedOperationException(); + } + + public Thread newVirtualThread(String name, Runnable task) { + throw new UnsupportedOperationException(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/VirtualThreadTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadTaskExecutor.java new file mode 100644 index 000000000000..b246428f1a3e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadTaskExecutor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task; + +import java.util.concurrent.ThreadFactory; + +/** + * A {@link TaskExecutor} implementation based on virtual threads in JDK 21+. + * The only configuration option is a thread name prefix. + * + *

    For additional features such as concurrency limiting or task decoration, + * consider using {@link SimpleAsyncTaskExecutor#setVirtualThreads} instead. + * + * @author Juergen Hoeller + * @since 6.1 + * @see SimpleAsyncTaskExecutor#setVirtualThreads + */ +public class VirtualThreadTaskExecutor implements AsyncTaskExecutor { + + private final ThreadFactory virtualThreadFactory; + + + /** + * Create a new {@code VirtualThreadTaskExecutor} without thread naming. + */ + public VirtualThreadTaskExecutor() { + this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory(); + } + + /** + * Create a new {@code VirtualThreadTaskExecutor} with thread names based + * on the given thread name prefix followed by a counter (e.g. "test-0"). + * @param threadNamePrefix the prefix for thread names (e.g. "test-") + */ + public VirtualThreadTaskExecutor(String threadNamePrefix) { + this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory(threadNamePrefix); + } + + + /** + * Return the underlying virtual {@link ThreadFactory}. + * Can also be used for custom thread creation elsewhere. + */ + public final ThreadFactory getVirtualThreadFactory() { + return this.virtualThreadFactory; + } + + @Override + public void execute(Runnable task) { + this.virtualThreadFactory.newThread(task).start(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/CompositeTaskDecorator.java b/spring-core/src/main/java/org/springframework/core/task/support/CompositeTaskDecorator.java new file mode 100644 index 000000000000..2431fecc3fda --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/CompositeTaskDecorator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.core.task.TaskDecorator; +import org.springframework.util.Assert; + +/** + * Composite {@link TaskDecorator} that delegates to other task decorators. + * + * @author Tadaya Tsuyukubo + * @since 6.1 + */ +public class CompositeTaskDecorator implements TaskDecorator { + + private final List taskDecorators; + + /** + * Create a new instance. + * @param taskDecorators the taskDecorators to delegate to + */ + public CompositeTaskDecorator(Collection taskDecorators) { + Assert.notNull(taskDecorators, "TaskDecorators must not be null"); + this.taskDecorators = new ArrayList<>(taskDecorators); + } + + @Override + public Runnable decorate(Runnable runnable) { + Assert.notNull(runnable, "Runnable must not be null"); + for (TaskDecorator taskDecorator : this.taskDecorators) { + runnable = taskDecorator.decorate(runnable); + } + return runnable; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java deleted file mode 100644 index 016b2d0460b4..000000000000 --- a/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.core.task.support; - -import java.util.concurrent.Executor; - -import org.springframework.core.task.TaskExecutor; -import org.springframework.util.Assert; - -/** - * Adapter that exposes the {@link java.util.concurrent.Executor} interface for - * any Spring {@link org.springframework.core.task.TaskExecutor}. - * - *

    This adapter is less useful since Spring 3.0, since TaskExecutor itself - * extends the {@code Executor} interface. The adapter is only relevant for - * hiding the {@code TaskExecutor} nature of a given object, solely - * exposing the standard {@code Executor} interface to a client. - * - * @author Juergen Hoeller - * @since 2.5 - * @see java.util.concurrent.Executor - * @see org.springframework.core.task.TaskExecutor - * @deprecated {@code ConcurrentExecutorAdapter} is obsolete and will be removed - * in Spring Framework 6.1 - */ -@Deprecated(since = "6.0.5", forRemoval = true) -public class ConcurrentExecutorAdapter implements Executor { - - private final TaskExecutor taskExecutor; - - - /** - * Create a new ConcurrentExecutorAdapter for the given Spring TaskExecutor. - * @param taskExecutor the Spring TaskExecutor to wrap - */ - public ConcurrentExecutorAdapter(TaskExecutor taskExecutor) { - Assert.notNull(taskExecutor, "TaskExecutor must not be null"); - this.taskExecutor = taskExecutor; - } - - - @Override - public void execute(Runnable command) { - this.taskExecutor.execute(command); - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ContextPropagatingTaskDecorator.java b/spring-core/src/main/java/org/springframework/core/task/support/ContextPropagatingTaskDecorator.java new file mode 100644 index 000000000000..67a5799ab877 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/ContextPropagatingTaskDecorator.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task.support; + +import io.micrometer.context.ContextSnapshot; +import io.micrometer.context.ContextSnapshotFactory; + +import org.springframework.core.task.TaskDecorator; + +/** + * {@link TaskDecorator} that {@link ContextSnapshot#wrap(Runnable) wraps the execution} + * of tasks, assisting with context propagation. + * + *

    This operation is only useful when the task execution is scheduled on a different + * thread than the original call stack; this depends on the choice of + * {@link org.springframework.core.task.TaskExecutor}. This is particularly useful for + * restoring a logging context or an observation context for the task execution. Note that + * this decorator will cause some overhead for task execution and is not recommended for + * applications that run lots of very small tasks. + * + * @author Brian Clozel + * @since 6.1 + * @see CompositeTaskDecorator + */ +public class ContextPropagatingTaskDecorator implements TaskDecorator { + + private final ContextSnapshotFactory factory; + + + /** + * Create a new decorator that uses a default instance of the {@link ContextSnapshotFactory}. + */ + public ContextPropagatingTaskDecorator() { + this(ContextSnapshotFactory.builder().build()); + } + + /** + * Create a new decorator using the given {@link ContextSnapshotFactory}. + * @param factory the context snapshot factory to use. + */ + public ContextPropagatingTaskDecorator(ContextSnapshotFactory factory) { + this.factory = factory; + } + + + @Override + public Runnable decorate(Runnable runnable) { + return this.factory.captureAll().wrap(runnable); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java index 9c8958d21428..e1860006a5eb 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java @@ -34,7 +34,7 @@ * *

    NOTE: This ExecutorService adapter does not support the * lifecycle methods in the {@code java.util.concurrent.ExecutorService} API - * ("shutdown()" etc), similar to a server-wide {@code ManagedExecutorService} + * ("shutdown()" etc.), similar to a server-wide {@code ManagedExecutorService} * in a Jakarta EE environment. The lifecycle is always up to the backend pool, * with this adapter acting as an access-only proxy for that target pool. * diff --git a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java index 2a9b3365208d..c7085ffa75ff 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -93,17 +93,10 @@ public void execute(Runnable task) { doExecute(this.concurrentExecutor, this.taskDecorator, task); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException( - "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(this.concurrentExecutor, task, ex); } } - @Deprecated - @Override - public void execute(Runnable task, long startTimeout) { - execute(task); - } - @Override public Future submit(Runnable task) { try { @@ -118,8 +111,7 @@ public Future submit(Runnable task) { } } catch (RejectedExecutionException ex) { - throw new TaskRejectedException( - "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(this.concurrentExecutor, task, ex); } } @@ -137,8 +129,7 @@ public Future submit(Callable task) { } } catch (RejectedExecutionException ex) { - throw new TaskRejectedException( - "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(this.concurrentExecutor, task, ex); } } @@ -150,8 +141,7 @@ public ListenableFuture submitListenable(Runnable task) { return future; } catch (RejectedExecutionException ex) { - throw new TaskRejectedException( - "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(this.concurrentExecutor, task, ex); } } @@ -163,8 +153,7 @@ public ListenableFuture submitListenable(Callable task) { return future; } catch (RejectedExecutionException ex) { - throw new TaskRejectedException( - "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + throw new TaskRejectedException(this.concurrentExecutor, task, ex); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java index b32e552a543b..68028ba5d4dd 100644 --- a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,15 @@ package org.springframework.core.type; import java.lang.annotation.Annotation; +import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotationCollectors; @@ -30,8 +37,9 @@ /** * Defines access to the annotations of a specific type ({@link AnnotationMetadata class} - * or {@link MethodMetadata method}), in a form that does not necessarily require the - * class-loading. + * or {@link MethodMetadata method}), in a form that does not necessarily require + * class loading of the types being inspected. Note, however, that classes for + * encountered annotations will be loaded. * * @author Juergen Hoeller * @author Mark Fisher @@ -46,9 +54,9 @@ public interface AnnotatedTypeMetadata { /** - * Return annotation details based on the direct annotations of the - * underlying element. - * @return merged annotations based on the direct annotations + * Get annotation details based on the direct annotations and meta-annotations + * of the underlying element. + * @return merged annotations based on the direct annotations and meta-annotations * @since 5.2 */ MergedAnnotations getAnnotations(); @@ -58,7 +66,7 @@ public interface AnnotatedTypeMetadata { * of the given type defined. *

    If this method returns {@code true}, then * {@link #getAnnotationAttributes} will return a non-null Map. - * @param annotationName the fully qualified class name of the annotation + * @param annotationName the fully-qualified class name of the annotation * type to look for * @return whether a matching annotation is defined */ @@ -68,13 +76,15 @@ default boolean isAnnotated(String annotationName) { /** * Retrieve the attributes of the annotation of the given type, if any (i.e. if - * defined on the underlying element, as direct annotation or meta-annotation), - * also taking attribute overrides on composed annotations into account. - * @param annotationName the fully qualified class name of the annotation + * defined on the underlying element, as direct annotation or meta-annotation). + *

    {@link org.springframework.core.annotation.AliasFor @AliasFor} semantics + * are fully supported, both within a single annotation and within annotation + * hierarchies. + * @param annotationName the fully-qualified class name of the annotation * type to look for - * @return a Map of attributes, with the attribute name as key (e.g. "value") - * and the defined attribute value as Map value. This return value will be - * {@code null} if no matching annotation is defined. + * @return a {@link Map} of attributes, with each annotation attribute name + * as map key (e.g. "location") and the attribute's value as map value; or + * {@code null} if no matching annotation is found */ @Nullable default Map getAnnotationAttributes(String annotationName) { @@ -83,16 +93,18 @@ default Map getAnnotationAttributes(String annotationName) { /** * Retrieve the attributes of the annotation of the given type, if any (i.e. if - * defined on the underlying element, as direct annotation or meta-annotation), - * also taking attribute overrides on composed annotations into account. - * @param annotationName the fully qualified class name of the annotation + * defined on the underlying element, as direct annotation or meta-annotation). + *

    {@link org.springframework.core.annotation.AliasFor @AliasFor} semantics + * are fully supported, both within a single annotation and within annotation + * hierarchies. + * @param annotationName the fully-qualified class name of the annotation * type to look for * @param classValuesAsString whether to convert class references to String * class names for exposure as values in the returned Map, instead of Class * references which might potentially have to be loaded first - * @return a Map of attributes, with the attribute name as key (e.g. "value") - * and the defined attribute value as Map value. This return value will be - * {@code null} if no matching annotation is defined. + * @return a {@link Map} of attributes, with each annotation attribute name + * as map key (e.g. "location") and the attribute's value as map value; or + * {@code null} if no matching annotation is found */ @Nullable default Map getAnnotationAttributes(String annotationName, @@ -109,12 +121,13 @@ default Map getAnnotationAttributes(String annotationName, /** * Retrieve all attributes of all annotations of the given type, if any (i.e. if * defined on the underlying element, as direct annotation or meta-annotation). - * Note that this variant does not take attribute overrides into account. - * @param annotationName the fully qualified class name of the annotation + *

    Note: this method does not take attribute overrides on composed + * annotations into account. + * @param annotationName the fully-qualified class name of the annotation * type to look for - * @return a MultiMap of attributes, with the attribute name as key (e.g. "value") - * and a list of the defined attribute values as Map value. This return value will - * be {@code null} if no matching annotation is defined. + * @return a {@link MultiValueMap} of attributes, with each annotation attribute + * name as map key (e.g. "location") and a list of the attribute's values as + * map value; or {@code null} if no matching annotation is found * @see #getAllAnnotationAttributes(String, boolean) */ @Nullable @@ -125,13 +138,16 @@ default MultiValueMap getAllAnnotationAttributes(String annotati /** * Retrieve all attributes of all annotations of the given type, if any (i.e. if * defined on the underlying element, as direct annotation or meta-annotation). - * Note that this variant does not take attribute overrides into account. - * @param annotationName the fully qualified class name of the annotation + *

    Note: this method does not take attribute overrides on composed + * annotations into account. + * @param annotationName the fully-qualified class name of the annotation * type to look for - * @param classValuesAsString whether to convert class references to String - * @return a MultiMap of attributes, with the attribute name as key (e.g. "value") - * and a list of the defined attribute values as Map value. This return value will - * be {@code null} if no matching annotation is defined. + * @param classValuesAsString whether to convert class references to String + * class names for exposure as values in the returned Map, instead of Class + * references which might potentially have to be loaded first + * @return a {@link MultiValueMap} of attributes, with each annotation attribute + * name as map key (e.g. "location") and a list of the attribute's values as + * map value; or {@code null} if no matching annotation is found * @see #getAllAnnotationAttributes(String) */ @Nullable @@ -142,8 +158,139 @@ default MultiValueMap getAllAnnotationAttributes( return getAnnotations().stream(annotationName) .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)) .map(MergedAnnotation::withNonMergedAttributes) - .collect(MergedAnnotationCollectors.toMultiValueMap(map -> - map.isEmpty() ? null : map, adaptations)); + .collect(MergedAnnotationCollectors.toMultiValueMap( + map -> (map.isEmpty() ? null : map), adaptations)); + } + + /** + * Retrieve all repeatable annotations of the given type within the + * annotation hierarchy above the underlying element (as direct + * annotation or meta-annotation); and for each annotation found, merge that + * annotation's attributes with matching attributes from annotations + * in lower levels of the annotation hierarchy and store the results in an + * instance of {@link AnnotationAttributes}. + *

    {@link org.springframework.core.annotation.AliasFor @AliasFor} semantics + * are fully supported, both within a single annotation and within annotation + * hierarchies. + * @param annotationType the annotation type to find + * @param containerType the type of the container that holds the annotations + * @param classValuesAsString whether to convert class references to {@code String} + * class names for exposure as values in the returned {@code AnnotationAttributes}, + * instead of {@code Class} references which might potentially have to be loaded + * first + * @return the set of all merged repeatable {@code AnnotationAttributes} found, + * or an empty set if none were found + * @since 6.1 + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean) + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, Predicate, boolean, boolean) + */ + default Set getMergedRepeatableAnnotationAttributes( + Class annotationType, Class containerType, + boolean classValuesAsString) { + + return getMergedRepeatableAnnotationAttributes(annotationType, containerType, classValuesAsString, false); + } + + /** + * Retrieve all repeatable annotations of the given type within the + * annotation hierarchy above the underlying element (as direct + * annotation or meta-annotation); and for each annotation found, merge that + * annotation's attributes with matching attributes from annotations + * in lower levels of the annotation hierarchy and store the results in an + * instance of {@link AnnotationAttributes}. + *

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

    If the {@code sortByReversedMetaDistance} flag is set to {@code true}, + * the results will be sorted in {@link Comparator#reversed() reversed} order + * based on each annotation's {@linkplain MergedAnnotation#getDistance() + * meta distance}, which effectively orders meta-annotations before annotations + * that are declared directly on the underlying element. + * @param annotationType the annotation type to find + * @param containerType the type of the container that holds the annotations + * @param classValuesAsString whether to convert class references to {@code String} + * class names for exposure as values in the returned {@code AnnotationAttributes}, + * instead of {@code Class} references which might potentially have to be loaded + * first + * @param sortByReversedMetaDistance {@code true} if the results should be + * sorted in reversed order based on each annotation's meta distance + * @return the set of all merged repeatable {@code AnnotationAttributes} found, + * or an empty set if none were found + * @since 6.1 + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean) + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, Predicate, boolean, boolean) + */ + default Set getMergedRepeatableAnnotationAttributes( + Class annotationType, Class containerType, + boolean classValuesAsString, boolean sortByReversedMetaDistance) { + + return getMergedRepeatableAnnotationAttributes(annotationType, containerType, + mergedAnnotation -> true, classValuesAsString, sortByReversedMetaDistance); + } + + /** + * Retrieve all repeatable annotations of the given type within the + * annotation hierarchy above the underlying element (as direct + * annotation or meta-annotation); and for each annotation found, merge that + * annotation's attributes with matching attributes from annotations + * in lower levels of the annotation hierarchy and store the results in an + * instance of {@link AnnotationAttributes}. + *

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

    The supplied {@link Predicate} will be used to filter the results. For + * example, supply {@code mergedAnnotation -> true} to include all annotations + * in the results; supply {@code MergedAnnotation::isDirectlyPresent} to limit + * the results to directly declared annotations, etc. + *

    If the {@code sortByReversedMetaDistance} flag is set to {@code true}, + * the results will be sorted in {@link Comparator#reversed() reversed} order + * based on each annotation's {@linkplain MergedAnnotation#getDistance() + * meta distance}, which effectively orders meta-annotations before annotations + * that are declared directly on the underlying element. + * @param annotationType the annotation type to find + * @param containerType the type of the container that holds the annotations + * @param predicate a {@code Predicate} to apply to each {@code MergedAnnotation} + * to determine if it should be included in the results + * @param classValuesAsString whether to convert class references to {@code String} + * class names for exposure as values in the returned {@code AnnotationAttributes}, + * instead of {@code Class} references which might potentially have to be loaded + * first + * @param sortByReversedMetaDistance {@code true} if the results should be + * sorted in reversed order based on each annotation's meta distance + * @return the set of all merged repeatable {@code AnnotationAttributes} found, + * or an empty set if none were found + * @since 6.1.2 + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean) + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean) + */ + default Set getMergedRepeatableAnnotationAttributes( + Class annotationType, Class containerType, + Predicate> predicate, boolean classValuesAsString, + boolean sortByReversedMetaDistance) { + + Stream> stream = getAnnotations().stream() + .filter(predicate) + .filter(MergedAnnotationPredicates.typeIn(containerType, annotationType)); + + if (sortByReversedMetaDistance) { + stream = stream.sorted(reversedMetaDistance()); + } + + Adapt[] adaptations = Adapt.values(classValuesAsString, true); + return stream + .map(annotation -> annotation.asAnnotationAttributes(adaptations)) + .flatMap(attributes -> { + if (containerType.equals(attributes.annotationType())) { + return Stream.of(attributes.getAnnotationArray(MergedAnnotation.VALUE)); + } + return Stream.of(attributes); + }) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + + private static Comparator> reversedMetaDistance() { + return Comparator.> comparingInt(MergedAnnotation::getDistance).reversed(); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java index 944e232af9f0..49a8f38eaa6d 100644 --- a/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; /** * Interface that defines abstract access to the annotations of a specific @@ -41,8 +40,8 @@ public interface AnnotationMetadata extends ClassMetadata, AnnotatedTypeMetadata { /** - * Get the fully qualified class names of all annotation types that - * are present on the underlying class. + * Get the fully-qualified class names of all annotation types that are + * directly present on the underlying class. * @return the annotation type names */ default Set getAnnotationTypes() { @@ -53,10 +52,10 @@ default Set getAnnotationTypes() { } /** - * Get the fully qualified class names of all meta-annotation types that - * are present on the given annotation type on the underlying class. - * @param annotationName the fully qualified class name of the meta-annotation - * type to look for + * Get the fully-qualified class names of all meta-annotation types that are + * present on the given annotation type on the underlying class. + * @param annotationName the fully-qualified class name of the annotation + * type to look for meta-annotations on * @return the meta-annotation type names, or an empty set if none found */ default Set getMetaAnnotationTypes(String annotationName) { @@ -64,17 +63,17 @@ default Set getMetaAnnotationTypes(String annotationName) { if (!annotation.isPresent()) { return Collections.emptySet(); } - return MergedAnnotations.from(annotation.getType(), SearchStrategy.INHERITED_ANNOTATIONS).stream() + return MergedAnnotations.from(annotation.getType()).stream() .map(mergedAnnotation -> mergedAnnotation.getType().getName()) .collect(Collectors.toCollection(LinkedHashSet::new)); } /** - * Determine whether an annotation of the given type is present on - * the underlying class. - * @param annotationName the fully qualified class name of the annotation + * Determine whether an annotation of the given type is directly present + * on the underlying class. + * @param annotationName the fully-qualified class name of the annotation * type to look for - * @return {@code true} if a matching annotation is present + * @return {@code true} if a matching annotation is directly present */ default boolean hasAnnotation(String annotationName) { return getAnnotations().isDirectlyPresent(annotationName); @@ -83,7 +82,7 @@ default boolean hasAnnotation(String annotationName) { /** * Determine whether the underlying class has an annotation that is itself * annotated with the meta-annotation of the given type. - * @param metaAnnotationName the fully qualified class name of the + * @param metaAnnotationName the fully-qualified class name of the * meta-annotation type to look for * @return {@code true} if a matching meta-annotation is present */ @@ -95,7 +94,7 @@ default boolean hasMetaAnnotation(String metaAnnotationName) { /** * Determine whether the underlying class has any methods that are * annotated (or meta-annotated) with the given annotation type. - * @param annotationName the fully qualified class name of the annotation + * @param annotationName the fully-qualified class name of the annotation * type to look for */ default boolean hasAnnotatedMethods(String annotationName) { @@ -107,7 +106,7 @@ default boolean hasAnnotatedMethods(String annotationName) { * (or meta-annotated) with the given annotation type. *

    For any returned method, {@link MethodMetadata#isAnnotated} will * return {@code true} for the given annotation type. - * @param annotationName the fully qualified class name of the annotation + * @param annotationName the fully-qualified class name of the annotation * type to look for * @return a set of {@link MethodMetadata} for methods that have a matching * annotation. The return value will be an empty set if no methods match diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/ClassFormatException.java b/spring-core/src/main/java/org/springframework/core/type/classreading/ClassFormatException.java new file mode 100644 index 000000000000..f45b8117997f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/ClassFormatException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.type.classreading; + +import java.io.IOException; + +import org.springframework.core.io.Resource; + +/** + * Exception that indicates an incompatible class format encountered + * in a class file during metadata reading. + * + * @author Juergen Hoeller + * @since 6.1.2 + * @see MetadataReaderFactory#getMetadataReader(Resource) + * @see ClassFormatError + */ +@SuppressWarnings("serial") +public class ClassFormatException extends IOException { + + /** + * Construct a new {@code ClassFormatException} with the + * supplied message. + * @param message the detail message + */ + public ClassFormatException(String message) { + super(message); + } + + /** + * Construct a new {@code ClassFormatException} with the + * supplied message and cause. + * @param message the detail message + * @param cause the root cause + */ + public ClassFormatException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java index 540ee3546a0a..1a2bfe899dfd 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java @@ -94,7 +94,7 @@ public AnnotationVisitor visitArray(String name) { @Override public void visitEnd() { Map compactedAttributes = - (this.attributes.size() == 0 ? Collections.emptyMap() : this.attributes); + (this.attributes.isEmpty() ? Collections.emptyMap() : this.attributes); MergedAnnotation annotation = MergedAnnotation.of( this.classLoader, this.source, this.annotationType, compactedAttributes); this.consumer.accept(annotation); diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java index 555b3acaa2ef..4eddbfa6cd24 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ public interface MetadataReaderFactory { * Obtain a MetadataReader for the given class name. * @param className the class name (to be resolved to a ".class" file) * @return a holder for the ClassReader instance (never {@code null}) + * @throws ClassFormatException in case of an incompatible class format * @throws IOException in case of I/O failure */ MetadataReader getMetadataReader(String className) throws IOException; @@ -43,6 +44,7 @@ public interface MetadataReaderFactory { * Obtain a MetadataReader for the given resource. * @param resource the resource (pointing to a ".class" file) * @return a holder for the ClassReader instance (never {@code null}) + * @throws ClassFormatException in case of an incompatible class format * @throws IOException in case of I/O failure */ MetadataReader getMetadataReader(Resource resource) throws IOException; diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java index 375ac630fffe..ba4c84f1c27f 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java @@ -56,7 +56,7 @@ final class SimpleAnnotationMetadata implements AnnotationMetadata { private final Set declaredMethods; - private final MergedAnnotations annotations; + private final MergedAnnotations mergedAnnotations; @Nullable private Set annotationTypes; @@ -64,7 +64,7 @@ final class SimpleAnnotationMetadata implements AnnotationMetadata { SimpleAnnotationMetadata(String className, int access, @Nullable String enclosingClassName, @Nullable String superClassName, boolean independentInnerClass, Set interfaceNames, - Set memberClassNames, Set declaredMethods, MergedAnnotations annotations) { + Set memberClassNames, Set declaredMethods, MergedAnnotations mergedAnnotations) { this.className = className; this.access = access; @@ -74,7 +74,7 @@ final class SimpleAnnotationMetadata implements AnnotationMetadata { this.interfaceNames = interfaceNames; this.memberClassNames = memberClassNames; this.declaredMethods = declaredMethods; - this.annotations = annotations; + this.mergedAnnotations = mergedAnnotations; } @Override @@ -131,7 +131,7 @@ public String[] getMemberClassNames() { @Override public MergedAnnotations getAnnotations() { - return this.annotations; + return this.mergedAnnotations; } @Override diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java index d89ae8f52e6a..08150a51a476 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,8 @@ */ final class SimpleMetadataReader implements MetadataReader { - private static final int PARSING_OPTIONS = ClassReader.SKIP_DEBUG - | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES; + private static final int PARSING_OPTIONS = + (ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); private final Resource resource; @@ -56,8 +56,10 @@ private static ClassReader getClassReader(Resource resource) throws IOException return new ClassReader(is); } catch (IllegalArgumentException ex) { - throw new IOException("ASM ClassReader failed to parse class file - " + - "probably due to a new Java class file version that isn't supported yet: " + resource, ex); + throw new ClassFormatException("ASM ClassReader failed to parse class file - " + + "probably due to a new Java class file version that is not supported yet. " + + "Consider compiling with a lower '-target' or upgrade your framework version. " + + "Affected class: " + resource, ex); } } } diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java index 96874edb2eac..ee7d6d5046e2 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,20 +73,20 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass. Boolean superClassMatch = matchSuperClass(superClassName); if (superClassMatch != null) { - if (superClassMatch.booleanValue()) { + if (superClassMatch) { return true; } } else { // Need to read superclass to determine a match... try { - if (match(metadata.getSuperClassName(), metadataReaderFactory)) { + if (match(superClassName, metadataReaderFactory)) { return true; } } catch (IOException ex) { if (logger.isDebugEnabled()) { - logger.debug("Could not read superclass [" + metadata.getSuperClassName() + + logger.debug("Could not read superclass [" + superClassName + "] of type-filtered class [" + metadata.getClassName() + "]"); } } @@ -99,7 +99,7 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass Boolean interfaceMatch = matchInterface(ifc); if (interfaceMatch != null) { - if (interfaceMatch.booleanValue()) { + if (interfaceMatch) { return true; } } diff --git a/spring-core/src/main/java/org/springframework/lang/NonNull.java b/spring-core/src/main/java/org/springframework/lang/NonNull.java index cd4ecde458e9..8ec9fb0334a0 100644 --- a/spring-core/src/main/java/org/springframework/lang/NonNull.java +++ b/spring-core/src/main/java/org/springframework/lang/NonNull.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,9 @@ *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. * - *

    Should be used at parameter, return value, and field level. Method overrides should - * repeat parent {@code @NonNull} annotations unless they behave differently. + *

    Should be used at the parameter, return value, and field level. Method + * overrides should repeat parent {@code @NonNull} annotations unless they behave + * differently. * *

    Use {@code @NonNullApi} (scope = parameters + return values) and/or {@code @NonNullFields} * (scope = fields) to set the default behavior to non-nullable in order to avoid annotating diff --git a/spring-core/src/main/java/org/springframework/lang/NonNullApi.java b/spring-core/src/main/java/org/springframework/lang/NonNullApi.java index 7bf99bda65ef..e2418426a6cf 100644 --- a/spring-core/src/main/java/org/springframework/lang/NonNullApi.java +++ b/spring-core/src/main/java/org/springframework/lang/NonNullApi.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. * - *

    Should be used at package level in association with {@link Nullable} - * annotations at parameter and return value level. + *

    Should be used at the package level in association with {@link Nullable} + * annotations at the parameter and return value level. * * @author Sebastien Deleuze * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/lang/NonNullFields.java b/spring-core/src/main/java/org/springframework/lang/NonNullFields.java index 49968b9cb8cd..3bbaba3b7ce1 100644 --- a/spring-core/src/main/java/org/springframework/lang/NonNullFields.java +++ b/spring-core/src/main/java/org/springframework/lang/NonNullFields.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. * - *

    Should be used at package level in association with {@link Nullable} - * annotations at field level. + *

    Should be used at the package level in association with {@link Nullable} + * annotations at the field level. * * @author Sebastien Deleuze * @since 5.0 diff --git a/spring-core/src/main/java/org/springframework/lang/Nullable.java b/spring-core/src/main/java/org/springframework/lang/Nullable.java index 3abedbdcc480..324d28899a98 100644 --- a/spring-core/src/main/java/org/springframework/lang/Nullable.java +++ b/spring-core/src/main/java/org/springframework/lang/Nullable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,14 +27,15 @@ import javax.annotation.meta.When; /** - * A common Spring annotation to declare that annotated elements can be {@code null} under - * some circumstance. + * A common Spring annotation to declare that annotated elements can be {@code null} + * under certain circumstances. * *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. * - *

    Should be used at parameter, return value, and field level. Methods override should - * repeat parent {@code @Nullable} annotations unless they behave differently. + *

    Should be used at the parameter, return value, and field level. Method + * overrides should repeat parent {@code @Nullable} annotations unless they behave + * differently. * *

    Can be used in association with {@code @NonNullApi} or {@code @NonNullFields} to * override the default non-nullable semantic to nullable. diff --git a/spring-core/src/main/java/org/springframework/objenesis/package-info.java b/spring-core/src/main/java/org/springframework/objenesis/package-info.java index 017681c78f09..166e59b9c837 100644 --- a/spring-core/src/main/java/org/springframework/objenesis/package-info.java +++ b/spring-core/src/main/java/org/springframework/objenesis/package-info.java @@ -1,6 +1,6 @@ /** * Spring's repackaging of - * Objenesis 3.2 + * Objenesis 3.4 * (with SpringObjenesis entry point; for internal use only). * *

    This repackaging technique avoids any potential conflicts with diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java index 396bc0b51882..7f0544e13954 100644 --- a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -402,7 +402,7 @@ private boolean isWildcardChar(char c) { protected String[] tokenizePattern(String pattern) { String[] tokenized = null; Boolean cachePatterns = this.cachePatterns; - if (cachePatterns == null || cachePatterns.booleanValue()) { + if (cachePatterns == null || cachePatterns) { tokenized = this.tokenizedPatternCache.get(pattern); } if (tokenized == null) { @@ -414,7 +414,7 @@ protected String[] tokenizePattern(String pattern) { deactivatePatternCache(); return tokenized; } - if (cachePatterns == null || cachePatterns.booleanValue()) { + if (cachePatterns == null || cachePatterns) { this.tokenizedPatternCache.put(pattern, tokenized); } } @@ -458,7 +458,7 @@ private boolean matchStrings(String pattern, String str, protected AntPathStringMatcher getStringMatcher(String pattern) { AntPathStringMatcher matcher = null; Boolean cachePatterns = this.cachePatterns; - if (cachePatterns == null || cachePatterns.booleanValue()) { + if (cachePatterns == null || cachePatterns) { matcher = this.stringMatcherCache.get(pattern); } if (matcher == null) { @@ -470,7 +470,7 @@ protected AntPathStringMatcher getStringMatcher(String pattern) { deactivatePatternCache(); return matcher; } - if (cachePatterns == null || cachePatterns.booleanValue()) { + if (cachePatterns == null || cachePatterns) { this.stringMatcherCache.put(pattern, matcher); } } diff --git a/spring-core/src/main/java/org/springframework/util/Assert.java b/spring-core/src/main/java/org/springframework/util/Assert.java index c006ea6634fd..ad045071d486 100644 --- a/spring-core/src/main/java/org/springframework/util/Assert.java +++ b/spring-core/src/main/java/org/springframework/util/Assert.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,17 +98,6 @@ public static void state(boolean expression, Supplier messageSupplier) { } } - /** - * Assert a boolean expression, throwing an {@code IllegalStateException} - * if the expression evaluates to {@code false}. - * @deprecated as of 4.3.7, in favor of {@link #state(boolean, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void state(boolean expression) { - state(expression, "[Assertion failed] - this state invariant must be true"); - } - /** * Assert a boolean expression, throwing an {@code IllegalArgumentException} * if the expression evaluates to {@code false}. @@ -141,17 +130,6 @@ public static void isTrue(boolean expression, Supplier messageSupplier) } } - /** - * Assert a boolean expression, throwing an {@code IllegalArgumentException} - * if the expression evaluates to {@code false}. - * @deprecated as of 4.3.7, in favor of {@link #isTrue(boolean, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void isTrue(boolean expression) { - isTrue(expression, "[Assertion failed] - this expression must be true"); - } - /** * Assert that an object is {@code null}. *

    Assert.isNull(value, "The value must be null");
    @@ -182,16 +160,6 @@ public static void isNull(@Nullable Object object, Supplier messageSuppl } } - /** - * Assert that an object is {@code null}. - * @deprecated as of 4.3.7, in favor of {@link #isNull(Object, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void isNull(@Nullable Object object) { - isNull(object, "[Assertion failed] - the object argument must be null"); - } - /** * Assert that an object is not {@code null}. *
    Assert.notNull(clazz, "The class must not be null");
    @@ -223,16 +191,6 @@ public static void notNull(@Nullable Object object, Supplier messageSupp } } - /** - * Assert that an object is not {@code null}. - * @deprecated as of 4.3.7, in favor of {@link #notNull(Object, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void notNull(@Nullable Object object) { - notNull(object, "[Assertion failed] - this argument is required; it must not be null"); - } - /** * Assert that the given String is not empty; that is, * it must not be {@code null} and not the empty String. @@ -268,18 +226,6 @@ public static void hasLength(@Nullable String text, Supplier messageSupp } } - /** - * Assert that the given String is not empty; that is, - * it must not be {@code null} and not the empty String. - * @deprecated as of 4.3.7, in favor of {@link #hasLength(String, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void hasLength(@Nullable String text) { - hasLength(text, - "[Assertion failed] - this String argument must have length; it must not be null or empty"); - } - /** * Assert that the given String contains valid text content; that is, it must not * be {@code null} and must contain at least one non-whitespace character. @@ -315,18 +261,6 @@ public static void hasText(@Nullable String text, Supplier messageSuppli } } - /** - * Assert that the given String contains valid text content; that is, it must not - * be {@code null} and must contain at least one non-whitespace character. - * @deprecated as of 4.3.7, in favor of {@link #hasText(String, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void hasText(@Nullable String text) { - hasText(text, - "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); - } - /** * Assert that the given text does not contain the given substring. *
    Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
    @@ -361,17 +295,6 @@ public static void doesNotContain(@Nullable String textToSearch, String substrin } } - /** - * Assert that the given text does not contain the given substring. - * @deprecated as of 4.3.7, in favor of {@link #doesNotContain(String, String, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void doesNotContain(@Nullable String textToSearch, String substring) { - doesNotContain(textToSearch, substring, - () -> "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); - } - /** * Assert that an array contains elements; that is, it must not be * {@code null} and must contain at least one element. @@ -404,17 +327,6 @@ public static void notEmpty(@Nullable Object[] array, Supplier messageSu } } - /** - * Assert that an array contains elements; that is, it must not be - * {@code null} and must contain at least one element. - * @deprecated as of 4.3.7, in favor of {@link #notEmpty(Object[], String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void notEmpty(@Nullable Object[] array) { - notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); - } - /** * Assert that an array contains no {@code null} elements. *

    Note: Does not complain if the array is empty! @@ -455,16 +367,6 @@ public static void noNullElements(@Nullable Object[] array, Supplier mes } } - /** - * Assert that an array contains no {@code null} elements. - * @deprecated as of 4.3.7, in favor of {@link #noNullElements(Object[], String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void noNullElements(@Nullable Object[] array) { - noNullElements(array, "[Assertion failed] - this array must not contain any null elements"); - } - /** * Assert that a collection contains elements; that is, it must not be * {@code null} and must contain at least one element. @@ -499,18 +401,6 @@ public static void notEmpty(@Nullable Collection collection, Supplier } } - /** - * Assert that a collection contains elements; that is, it must not be - * {@code null} and must contain at least one element. - * @deprecated as of 4.3.7, in favor of {@link #notEmpty(Collection, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void notEmpty(@Nullable Collection collection) { - notEmpty(collection, - "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); - } - /** * Assert that a collection contains no {@code null} elements. *

    Note: Does not complain if the collection is empty! @@ -584,17 +474,6 @@ public static void notEmpty(@Nullable Map map, Supplier messageSup } } - /** - * Assert that a Map contains entries; that is, it must not be {@code null} - * and must contain at least one entry. - * @deprecated as of 4.3.7, in favor of {@link #notEmpty(Map, String)}; - * to be removed in 6.1 - */ - @Deprecated(forRemoval = true) - public static void notEmpty(@Nullable Map map) { - notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry"); - } - /** * Assert that the provided object is an instance of the provided class. *

    Assert.instanceOf(Foo.class, foo, "Foo expected");
    diff --git a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java index ff966cd2926a..4a01bd502128 100644 --- a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java +++ b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,7 +138,7 @@ public boolean containsAll(Collection c) { @Override public E get(int index) { int backingListSize = this.backingList.size(); - E element = null; + E element; if (index < backingListSize) { element = this.backingList.get(index); if (element == null) { diff --git a/spring-core/src/main/java/org/springframework/util/Base64Utils.java b/spring-core/src/main/java/org/springframework/util/Base64Utils.java index 0403c2999074..49cdef9896d4 100644 --- a/spring-core/src/main/java/org/springframework/util/Base64Utils.java +++ b/spring-core/src/main/java/org/springframework/util/Base64Utils.java @@ -98,7 +98,7 @@ public static String encodeToString(byte[] src) { } /** - * Base64-decode the given byte array from an UTF-8 String. + * Base64-decode the given byte array from a UTF-8 String. * @param src the encoded UTF-8 String * @return the original byte array */ @@ -120,7 +120,7 @@ public static String encodeToUrlSafeString(byte[] src) { } /** - * Base64-decode the given byte array from an UTF-8 String using the RFC 4648 + * Base64-decode the given byte array from a UTF-8 String using the RFC 4648 * "URL and Filename Safe Alphabet". * @param src the encoded UTF-8 String * @return the original byte array diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index e4d9a4ca0b68..93692fa678e4 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,24 @@ import java.io.Closeable; import java.io.Externalizable; +import java.io.File; import java.io.Serializable; -import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.ZoneId; +import java.time.temporal.Temporal; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Currency; +import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -34,10 +43,14 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.StringJoiner; +import java.util.TimeZone; +import java.util.UUID; +import java.util.regex.Pattern; import org.springframework.lang.Nullable; @@ -93,19 +106,19 @@ public abstract class ClassUtils { /** * Map with primitive wrapper type as key and corresponding primitive - * type as value, for example: Integer.class -> int.class. + * type as value, for example: {@code Integer.class -> int.class}. */ private static final Map, Class> primitiveWrapperTypeMap = new IdentityHashMap<>(9); /** * Map with primitive type as key and corresponding wrapper - * type as value, for example: int.class -> Integer.class. + * type as value, for example: {@code int.class -> Integer.class}. */ private static final Map, Class> primitiveTypeToWrapperMap = new IdentityHashMap<>(9); /** * Map with primitive type name as key and corresponding primitive - * type as value, for example: "int" -> "int.class". + * type as value, for example: {@code "int" -> int.class}. */ private static final Map> primitiveTypeNameMap = new HashMap<>(32); @@ -243,7 +256,7 @@ public static ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State"). * @param name the name of the Class * @param classLoader the class loader to use - * (may be {@code null}, which indicates the default class loader) + * (can be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws ClassNotFoundException if the class was not found * @throws LinkageError if the class file could not be loaded @@ -266,21 +279,21 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) if (name.endsWith(ARRAY_SUFFIX)) { String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length()); Class elementClass = forName(elementClassName, classLoader); - return Array.newInstance(elementClass, 0).getClass(); + return elementClass.arrayType(); } // "[Ljava.lang.String;" style arrays if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); Class elementClass = forName(elementName, classLoader); - return Array.newInstance(elementClass, 0).getClass(); + return elementClass.arrayType(); } // "[[I" or "[[Ljava.lang.String;" style arrays if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); Class elementClass = forName(elementName, classLoader); - return Array.newInstance(elementClass, 0).getClass(); + return elementClass.arrayType(); } ClassLoader clToUse = classLoader; @@ -292,7 +305,8 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) } catch (ClassNotFoundException ex) { int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); - if (lastDotIndex != -1) { + int previousDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR, lastDotIndex - 1); + if (lastDotIndex != -1 && previousDotIndex != -1 && Character.isUpperCase(name.charAt(previousDotIndex + 1))) { String nestedClassName = name.substring(0, lastDotIndex) + NESTED_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); try { @@ -314,7 +328,7 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) * the exceptions thrown in case of class loading failure. * @param className the name of the Class * @param classLoader the class loader to use - * (may be {@code null}, which indicates the default class loader) + * (can be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws IllegalArgumentException if the class name was not resolvable * (that is, the class could not be found or the class file could not be loaded) @@ -348,7 +362,7 @@ public static Class resolveClassName(String className, @Nullable ClassLoader * one of its dependencies is not present or cannot be loaded. * @param className the name of the class to check * @param classLoader the class loader to use - * (may be {@code null} which indicates the default class loader) + * (can be {@code null} which indicates the default class loader) * @return whether the specified class is present (including all of its * superclasses and interfaces) * @throws IllegalStateException if the corresponding class is resolvable but @@ -375,7 +389,7 @@ public static boolean isPresent(String className, @Nullable ClassLoader classLoa * Check whether the given class is visible in the given ClassLoader. * @param clazz the class to check (typically an interface) * @param classLoader the ClassLoader to check against - * (may be {@code null} in which case this method will always return {@code true}) + * (can be {@code null} in which case this method will always return {@code true}) */ public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoader) { if (classLoader == null) { @@ -399,7 +413,7 @@ public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoade * i.e. whether it is loaded by the given ClassLoader or a parent of it. * @param clazz the class to analyze * @param classLoader the ClassLoader to potentially cache metadata in - * (may be {@code null} which indicates the system class loader) + * (can be {@code null} which indicates the system class loader) */ public static boolean isCacheSafe(Class clazz, @Nullable ClassLoader classLoader) { Assert.notNull(clazz, "Class must not be null"); @@ -510,7 +524,7 @@ public static boolean isPrimitiveOrWrapper(Class clazz) { */ public static boolean isPrimitiveArray(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - return (clazz.isArray() && clazz.getComponentType().isPrimitive()); + return (clazz.isArray() && clazz.componentType().isPrimitive()); } /** @@ -521,7 +535,7 @@ public static boolean isPrimitiveArray(Class clazz) { */ public static boolean isPrimitiveWrapperArray(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType())); + return (clazz.isArray() && isPrimitiveWrapper(clazz.componentType())); } /** @@ -535,6 +549,56 @@ public static Class resolvePrimitiveIfNecessary(Class clazz) { return (clazz.isPrimitive() && clazz != void.class ? primitiveTypeToWrapperMap.get(clazz) : clazz); } + /** + * Determine if the given type represents either {@code Void} or {@code void}. + * @param type the type to check + * @return {@code true} if the type represents {@code Void} or {@code void} + * @since 6.1.4 + * @see Void + * @see Void#TYPE + */ + public static boolean isVoidType(@Nullable Class type) { + return (type == void.class || type == Void.class); + } + + /** + * Delegate for {@link org.springframework.beans.BeanUtils#isSimpleValueType}. + * Also used by {@link ObjectUtils#nullSafeConciseToString}. + *

    Check if the given type represents a common "simple" value type: + * primitive or primitive wrapper, {@link Enum}, {@link String} or other + * {@link CharSequence}, {@link Number}, {@link Date}, {@link Temporal}, + * {@link ZoneId}, {@link TimeZone}, {@link File}, {@link Path}, {@link URI}, + * {@link URL}, {@link InetAddress}, {@link Charset}, {@link Currency}, + * {@link Locale}, {@link UUID}, {@link Pattern}, or {@link Class}. + *

    {@code Void} and {@code void} are not considered simple value types. + * @param type the type to check + * @return whether the given type represents a "simple" value type, + * suggesting value-based data binding and {@code toString} output + * @since 6.1 + */ + public static boolean isSimpleValueType(Class type) { + return (!isVoidType(type) && + (isPrimitiveOrWrapper(type) || + Enum.class.isAssignableFrom(type) || + CharSequence.class.isAssignableFrom(type) || + Number.class.isAssignableFrom(type) || + Date.class.isAssignableFrom(type) || + Temporal.class.isAssignableFrom(type) || + ZoneId.class.isAssignableFrom(type) || + TimeZone.class.isAssignableFrom(type) || + File.class.isAssignableFrom(type) || + Path.class.isAssignableFrom(type) || + Charset.class.isAssignableFrom(type) || + Currency.class.isAssignableFrom(type) || + InetAddress.class.isAssignableFrom(type) || + URI.class == type || + URL.class == type || + UUID.class == type || + Locale.class == type || + Pattern.class == type || + Class.class == type)); + } + /** * Check if the right-hand side type may be assigned to the left-hand side * type, assuming setting by reflection. Considers primitive wrapper @@ -663,7 +727,7 @@ public static String classNamesToString(Class... classes) { * in the given collection. *

    Basically like {@code AbstractCollection.toString()}, but stripping * the "class "/"interface " prefix before every class name. - * @param classes a Collection of Class objects (may be {@code null}) + * @param classes a Collection of Class objects (can be {@code null}) * @return a String of form "[com.foo.Bar, com.foo.Baz]" * @see java.util.AbstractCollection#toString() */ @@ -718,7 +782,7 @@ public static Class[] getAllInterfacesForClass(Class clazz) { *

    If the class itself is an interface, it gets returned as sole interface. * @param clazz the class to analyze for interfaces * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) + * (can be {@code null} when accepting all declared interfaces) * @return all interfaces that the given object implements as an array */ public static Class[] getAllInterfacesForClass(Class clazz, @Nullable ClassLoader classLoader) { @@ -753,7 +817,7 @@ public static Set> getAllInterfacesForClassAsSet(Class clazz) { *

    If the class itself is an interface, it gets returned as sole interface. * @param clazz the class to analyze for interfaces * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) + * (can be {@code null} when accepting all declared interfaces) * @return all interfaces that the given object implements as a Set */ public static Set> getAllInterfacesForClassAsSet(Class clazz, @Nullable ClassLoader classLoader) { @@ -1082,7 +1146,7 @@ public static String getQualifiedMethodName(Method method) { * fully qualified interface/class name + "." + method name. * @param method the method * @param clazz the clazz that the method is being invoked on - * (may be {@code null} to indicate the method's declaring class) + * (can be {@code null} to indicate the method's declaring class) * @return the qualified name of the method * @since 4.3.4 */ @@ -1163,7 +1227,7 @@ public static boolean hasMethod(Class clazz, String methodName, Class... p * @param clazz the clazz to analyze * @param methodName the name of the method * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) + * (can be {@code null} to indicate any signature) * @return the method (never {@code null}) * @throws IllegalStateException if the method has not been found * @see Class#getMethod @@ -1202,7 +1266,7 @@ else if (candidates.isEmpty()) { * @param clazz the clazz to analyze * @param methodName the name of the method * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) + * (can be {@code null} to indicate any signature) * @return the method, or {@code null} if not found * @see Class#getMethod */ @@ -1291,13 +1355,14 @@ public static boolean hasAtLeastOneMethodWithName(Class clazz, String methodN * implementation will fall back to returning the originally provided method. * @param method the method to be invoked, which may come from an interface * @param targetClass the target class for the current invocation - * (may be {@code null} or may not even implement the method) + * (can be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the * {@code targetClass} does not implement it * @see #getInterfaceMethodIfPossible(Method, Class) */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { - if (targetClass != null && targetClass != method.getDeclaringClass() && isOverridable(method, targetClass)) { + if (targetClass != null && targetClass != method.getDeclaringClass() && + (isOverridable(method, targetClass) || !method.getDeclaringClass().isAssignableFrom(targetClass))) { try { if (Modifier.isPublic(method.getModifiers())) { try { @@ -1361,12 +1426,17 @@ public static Method getInterfaceMethodIfPossible(Method method, @Nullable Class } private static Method findInterfaceMethodIfPossible(Method method, Class startClass, Class endClass) { + Class[] parameterTypes = null; Class current = startClass; while (current != null && current != endClass) { - Class[] ifcs = current.getInterfaces(); - for (Class ifc : ifcs) { + if (parameterTypes == null) { + // Since Method#getParameterTypes() clones the array, we lazily retrieve + // and cache parameter types to avoid cloning the array multiple times. + parameterTypes = method.getParameterTypes(); + } + for (Class ifc : current.getInterfaces()) { try { - return ifc.getMethod(method.getName(), method.getParameterTypes()); + return ifc.getMethod(method.getName(), parameterTypes); } catch (NoSuchMethodException ex) { // ignore diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index 86156fef3bf5..bf47d0b917fd 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -478,5 +478,4 @@ public static MultiValueMap unmodifiableMultiValueMap( return new UnmodifiableMultiValueMap<>(targetMap); } - } diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java index 370537bf9a38..46da8e430ca3 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -60,7 +63,9 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { /** Transient to optimize serialization. */ protected transient Log logger = LogFactory.getLog(getClass()); - private transient Object monitor = new Object(); + private final Lock concurrencyLock = new ReentrantLock(); + + private final Condition concurrencyCondition = this.concurrencyLock.newCondition(); private int concurrencyLimit = UNBOUNDED_CONCURRENCY; @@ -109,7 +114,8 @@ protected void beforeAccess() { } if (this.concurrencyLimit > 0) { boolean debug = logger.isDebugEnabled(); - synchronized (this.monitor) { + this.concurrencyLock.lock(); + try { boolean interrupted = false; while (this.concurrencyCount >= this.concurrencyLimit) { if (interrupted) { @@ -121,7 +127,7 @@ protected void beforeAccess() { " has reached limit " + this.concurrencyLimit + " - blocking"); } try { - this.monitor.wait(); + this.concurrencyCondition.await(); } catch (InterruptedException ex) { // Re-interrupt current thread, to allow other threads to react. @@ -134,6 +140,9 @@ protected void beforeAccess() { } this.concurrencyCount++; } + finally { + this.concurrencyLock.unlock(); + } } } @@ -144,12 +153,16 @@ protected void beforeAccess() { protected void afterAccess() { if (this.concurrencyLimit >= 0) { boolean debug = logger.isDebugEnabled(); - synchronized (this.monitor) { + this.concurrencyLock.lock(); + try { this.concurrencyCount--; if (debug) { logger.debug("Returning from throttle at concurrency count " + this.concurrencyCount); } - this.monitor.notify(); + this.concurrencyCondition.signal(); + } + finally { + this.concurrencyLock.unlock(); } } } @@ -165,7 +178,6 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound // Initialize transient fields. this.logger = LogFactory.getLog(getClass()); - this.monitor = new Object(); } } diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java index 4d014b1a2b11..6ab26b8f6153 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java @@ -396,7 +396,6 @@ private static int detectNumberOfBuffers() { private final EvictionQueue evictionQueue; - @SuppressWarnings("rawtypes") ReadOperations(EvictionQueue evictionQueue) { this.evictionQueue = evictionQueue; for (int i = 0; i < BUFFER_COUNT; i++) { @@ -404,6 +403,7 @@ private static int detectNumberOfBuffers() { } } + @SuppressWarnings("deprecation") // for Thread.getId() on JDK 19 private static int getBufferIndex() { return ((int) Thread.currentThread().getId()) & BUFFERS_MASK; } @@ -418,6 +418,7 @@ boolean recordRead(Node node) { return (pending < MAX_PENDING_OPERATIONS); } + @SuppressWarnings("deprecation") // for Thread.getId() on JDK 19 void drain() { final int start = (int) Thread.currentThread().getId(); final int end = start + BUFFER_COUNT; diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 7dcebf7a68df..b826b3125311 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -317,7 +317,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry) } @Override - public boolean remove(@Nullable Object key, final @Nullable Object value) { + public boolean remove(@Nullable Object key, @Nullable final Object value) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -334,7 +334,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e } @Override - public boolean replace(@Nullable K key, final @Nullable V oldValue, final @Nullable V newValue) { + public boolean replace(@Nullable K key, @Nullable final V oldValue, @Nullable final V newValue) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -350,7 +350,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e @Override @Nullable - public V replace(@Nullable K key, final @Nullable V value) { + public V replace(@Nullable K key, @Nullable final V value) { return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override @Nullable @@ -603,38 +603,52 @@ private void restructure(boolean allowResize, @Nullable Reference ref) { resizing = true; } - // Either create a new table or reuse the existing one - Reference[] restructured = - (resizing ? createReferenceArray(restructureSize) : this.references); - - // Restructure int newCount = 0; - for (int i = 0; i < this.references.length; i++) { - ref = this.references[i]; - if (!resizing) { - restructured[i] = null; - } - while (ref != null) { - if (!toPurge.contains(ref)) { - Entry entry = ref.get(); - // Also filter out null references that are now null - // they should be polled the queue in a later restructure call. - if (entry != null) { - int index = getIndex(ref.getHash(), restructured); - restructured[index] = this.referenceManager.createReference( - entry, ref.getHash(), restructured[index]); - newCount++; + // Restructure the resized reference array + if (resizing) { + Reference[] restructured = createReferenceArray(restructureSize); + for (Reference reference : this.references) { + ref = reference; + while (ref != null) { + if (!toPurge.contains(ref)) { + Entry entry = ref.get(); + // Also filter out null references that are now null + // they should be polled from the queue in a later restructure call. + if (entry != null) { + int index = getIndex(ref.getHash(), restructured); + restructured[index] = this.referenceManager.createReference( + entry, ref.getHash(), restructured[index]); + newCount++; + } } + ref = ref.getNext(); } - ref = ref.getNext(); } - } - - // Replace volatile members - if (resizing) { + // Replace volatile members this.references = restructured; this.resizeThreshold = (int) (this.references.length * getLoadFactor()); } + // Restructure the existing reference array "in place" + else { + for (int i = 0; i < this.references.length; i++) { + Reference purgedRef = null; + ref = this.references[i]; + while (ref != null) { + if (!toPurge.contains(ref)) { + Entry entry = ref.get(); + // Also filter out null references that are now null + // they should be polled from the queue in a later restructure call. + if (entry != null) { + purgedRef = this.referenceManager.createReference( + entry, ref.getHash(), purgedRef); + } + newCount++; + } + ref = ref.getNext(); + } + this.references[i] = purgedRef; + } + } this.count.set(Math.max(newCount, 0)); } finally { diff --git a/spring-core/src/main/java/org/springframework/util/DigestUtils.java b/spring-core/src/main/java/org/springframework/util/DigestUtils.java index 63554c65b218..ce1a7d5184e7 100644 --- a/spring-core/src/main/java/org/springframework/util/DigestUtils.java +++ b/spring-core/src/main/java/org/springframework/util/DigestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,13 +125,13 @@ private static byte[] digest(String algorithm, byte[] bytes) { private static byte[] digest(String algorithm, InputStream inputStream) throws IOException { MessageDigest messageDigest = getDigest(algorithm); - if (inputStream instanceof UpdateMessageDigestInputStream digestIntputStream){ - digestIntputStream.updateMessageDigest(messageDigest); + if (inputStream instanceof UpdateMessageDigestInputStream digestInputStream){ + digestInputStream.updateMessageDigest(messageDigest); return messageDigest.digest(); } else { final byte[] buffer = new byte[StreamUtils.BUFFER_SIZE]; - int bytesRead = -1; + int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { messageDigest.update(buffer, 0, bytesRead); } diff --git a/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java b/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java index e2673124aa15..7e7f1430c7f0 100644 --- a/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java +++ b/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; import java.security.MessageDigest; import java.util.ArrayDeque; import java.util.Deque; @@ -32,12 +33,12 @@ * its sibling {@link ResizableByteArrayOutputStream}. * *

    Unlike {@link java.io.ByteArrayOutputStream}, this implementation is backed - * by a {@link java.util.ArrayDeque} of {@code byte[]} instead of 1 constantly - * resizing {@code byte[]}. It does not copy buffers when it gets expanded. + * by a {@link java.util.ArrayDeque} of {@code byte[]} buffers instead of one + * constantly resizing {@code byte[]}. It does not copy buffers when it gets expanded. * *

    The initial buffer is only created when the stream is first written. - * There is also no copying of the internal buffer if its content is extracted - * with the {@link #writeTo(OutputStream)} method. + * There is also no copying of the internal buffers if the stream's content is + * extracted via the {@link #writeTo(OutputStream)} method. * * @author Craig Andrews * @author Juergen Hoeller @@ -71,16 +72,16 @@ public class FastByteArrayOutputStream extends OutputStream { /** - * Create a new FastByteArrayOutputStream - * with the default initial capacity of 256 bytes. + * Create a new {@code FastByteArrayOutputStream} with the default initial + * capacity of 256 bytes. */ public FastByteArrayOutputStream() { this(DEFAULT_BLOCK_SIZE); } /** - * Create a new FastByteArrayOutputStream - * with the specified initial capacity. + * Create a new {@code FastByteArrayOutputStream} with the specified initial + * capacity. * @param initialBlockSize the initial buffer size in bytes */ public FastByteArrayOutputStream(int initialBlockSize) { @@ -149,34 +150,52 @@ public void close() { } /** - * Convert the buffer's contents into a string decoding bytes using the + * Convert this stream's contents to a string by decoding the bytes using the * platform's default character set. The length of the new {@code String} * is a function of the character set, and hence may not be equal to the - * size of the buffer. + * size of the buffers. *

    This method always replaces malformed-input and unmappable-character * sequences with the default replacement string for the platform's * default character set. The {@linkplain java.nio.charset.CharsetDecoder} * class should be used when more control over the decoding process is * required. - * @return a String decoded from the buffer's contents + * @return a String decoded from this stream's contents + * @see #toString(Charset) */ @Override public String toString() { - return new String(toByteArrayUnsafe()); + return toString(Charset.defaultCharset()); } + /** + * Convert this stream's contents to a string by decoding the bytes using the + * specified {@link Charset}. + * @param charset the {@link Charset} to use to decode the bytes + * @return a String decoded from this stream's contents + * @since 6.1.2 + * @see #toString() + */ + public String toString(Charset charset) { + if (size() == 0) { + return ""; + } + if (this.buffers.size() == 1) { + return new String(this.buffers.getFirst(), 0, this.index, charset); + } + return new String(toByteArrayUnsafe(), charset); + } // Custom methods /** - * Return the number of bytes stored in this FastByteArrayOutputStream. + * Return the number of bytes stored in this {@code FastByteArrayOutputStream}. */ public int size() { return (this.alreadyBufferedSize + this.index); } /** - * Convert the stream's data to a byte array and return the byte array. + * Convert this stream's contents to a byte array and return the byte array. *

    Also replaces the internal structures with the byte array to * conserve memory: if the byte array is being created anyway, we might * as well as use it. This approach also means that if this method is @@ -184,7 +203,7 @@ public int size() { * a no-op. *

    This method is "unsafe" as it returns the internal buffer. * Callers should not modify the returned buffer. - * @return the current contents of this output stream, as a byte array. + * @return the current contents of this stream as a byte array * @see #size() * @see #toByteArray() */ @@ -200,8 +219,8 @@ public byte[] toByteArrayUnsafe() { /** * Create a newly allocated byte array. *

    Its size is the current size of this output stream, and it will - * contain the valid contents of the internal buffer. - * @return the current contents of this output stream, as a byte array + * contain the valid contents of the internal buffers. + * @return the current contents of this stream as a byte array * @see #size() * @see #toByteArrayUnsafe() */ @@ -211,7 +230,7 @@ public byte[] toByteArray() { } /** - * Reset the contents of this FastByteArrayOutputStream. + * Reset the contents of this {@code FastByteArrayOutputStream}. *

    All currently accumulated output in the output stream is discarded. * The output stream can be used again. */ @@ -224,19 +243,21 @@ public void reset() { } /** - * Get an {@link InputStream} to retrieve the data in this OutputStream. - *

    Note that if any methods are called on the OutputStream + * Get an {@link InputStream} to retrieve the contents of this + * {@code FastByteArrayOutputStream}. + *

    Note that if any methods are called on this {@code FastByteArrayOutputStream} * (including, but not limited to, any of the write methods, {@link #reset()}, * {@link #toByteArray()}, and {@link #toByteArrayUnsafe()}) then the - * {@link java.io.InputStream}'s behavior is undefined. - * @return {@link InputStream} of the contents of this OutputStream + * {@code InputStream}'s behavior is undefined. + * @return {@code InputStream} of the contents of this {@code FastByteArrayOutputStream} */ public InputStream getInputStream() { return new FastByteArrayInputStream(this); } /** - * Write the buffers content to the given OutputStream. + * Write the contents of this {@code FastByteArrayOutputStream} to the given + * {@link OutputStream}. * @param out the OutputStream to write to */ public void writeTo(OutputStream out) throws IOException { @@ -253,7 +274,7 @@ public void writeTo(OutputStream out) throws IOException { } /** - * Resize the internal buffer size to a specified capacity. + * Resize the internal buffer size to the specified capacity. * @param targetCapacity the desired size of the buffer * @throws IllegalArgumentException if the given capacity is smaller than * the actual size of the content stored in the buffer already @@ -322,7 +343,7 @@ private static int nextPowerOf2(int val) { /** * An implementation of {@link java.io.InputStream} that reads from a given - * FastByteArrayOutputStream. + * {@code FastByteArrayOutputStream}. */ private static final class FastByteArrayInputStream extends UpdateMessageDigestInputStream { @@ -340,8 +361,8 @@ private static final class FastByteArrayInputStream extends UpdateMessageDigestI private int totalBytesRead = 0; /** - * Create a new FastByteArrayOutputStreamInputStream backed - * by the given FastByteArrayOutputStream. + * Create a new {@code FastByteArrayInputStream} backed by the given + * {@code FastByteArrayOutputStream}. */ public FastByteArrayInputStream(FastByteArrayOutputStream fastByteArrayOutputStream) { this.fastByteArrayOutputStream = fastByteArrayOutputStream; diff --git a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java index b931a0729591..d060c1c1187e 100644 --- a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java +++ b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java @@ -84,12 +84,13 @@ public static boolean deleteRecursively(@Nullable Path root) throws IOException return false; } - Files.walkFileTree(root, new SimpleFileVisitor() { + Files.walkFileTree(root, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } + @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); @@ -126,12 +127,13 @@ public static void copyRecursively(Path src, Path dest) throws IOException { BasicFileAttributes srcAttr = Files.readAttributes(src, BasicFileAttributes.class); if (srcAttr.isDirectory()) { - Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor() { + Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Files.createDirectories(dest.resolve(src.relativize(dir))); return FileVisitResult.CONTINUE; } + @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING); diff --git a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java index 0443c5699ca1..795922f5ab0e 100644 --- a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java +++ b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java @@ -136,7 +136,7 @@ public void setStaticMethod(String staticMethod) { * Set arguments for the method invocation. If this property is not set, * or the Object array is of length 0, a method with no arguments is assumed. */ - public void setArguments(Object... arguments) { + public void setArguments(@Nullable Object... arguments) { this.arguments = arguments; } diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java index 1043279d9685..a178489598aa 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeType.java +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -102,7 +102,6 @@ public class MimeType implements Comparable, Serializable { private final String subtype; - @SuppressWarnings("serial") private final Map parameters; @Nullable @@ -642,7 +641,7 @@ else if (getType().equals(other.getType()) && getSubtype().equals(other.getSubty } /** - * Indicates whether this {@code MimeType} is more less than the given type. + * Indicates whether this {@code MimeType} is less specific than the given type. *

      *
    1. if this mime type has a {@linkplain #isWildcardType() wildcard type}, * and the other does not, then this method returns {@code true}.
    2. @@ -684,7 +683,7 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound /** * Parse the given String value into a {@code MimeType} object, * with this method name following the 'valueOf' naming convention - * (as supported by {@link org.springframework.core.convert.ConversionService}. + * (as supported by {@link org.springframework.core.convert.ConversionService}). * @see MimeTypeUtils#parseMimeType(String) */ public static MimeType valueOf(String value) { diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index fb23809994dd..494d3fcc77d7 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -330,10 +330,10 @@ public static List tokenize(String mimeTypes) { } /** - * Return a string representation of the given list of {@code MimeType} objects. - * @param mimeTypes the string to parse - * @return the list of mime types - * @throws IllegalArgumentException if the String cannot be parsed + * Generate a string representation of the given collection of {@link MimeType} + * objects. + * @param mimeTypes the {@code MimeType} objects + * @return a string representation of the {@code MimeType} objects */ public static String toString(Collection mimeTypes) { StringBuilder builder = new StringBuilder(); @@ -348,21 +348,19 @@ public static String toString(Collection mimeTypes) { } /** - * Sorts the given list of {@code MimeType} objects by + * Sort the given list of {@code MimeType} objects by * {@linkplain MimeType#isMoreSpecific(MimeType) specificity}. - * - *

      Because of the computational cost, this method throws an exception - * when the given list contains too many elements. + *

      Because of the computational cost, this method throws an exception if + * the given list contains too many elements. * @param mimeTypes the list of mime types to be sorted - * @throws IllegalArgumentException if {@code mimeTypes} contains more - * than 50 elements + * @throws InvalidMimeTypeException if {@code mimeTypes} contains more than 50 elements * @see HTTP 1.1: Semantics * and Content, section 5.3.2 * @see MimeType#isMoreSpecific(MimeType) */ public static void sortBySpecificity(List mimeTypes) { Assert.notNull(mimeTypes, "'mimeTypes' must not be null"); - if (mimeTypes.size() >= 50) { + if (mimeTypes.size() > 50) { throw new InvalidMimeTypeException(mimeTypes.toString(), "Too many elements"); } diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java index 8c158ecf0fc4..4c7c2f1d133c 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ public MultiValueMapAdapter(Map> targetMap) { @Nullable public V getFirst(K key) { List values = this.targetMap.get(key); - return (values != null && !values.isEmpty() ? values.get(0) : null); + return (!CollectionUtils.isEmpty(values) ? values.get(0) : null); } @Override @@ -95,7 +95,7 @@ public void setAll(Map values) { public Map toSingleValueMap() { Map singleValueMap = CollectionUtils.newLinkedHashMap(this.targetMap.size()); this.targetMap.forEach((key, values) -> { - if (values != null && !values.isEmpty()) { + if (!CollectionUtils.isEmpty(values)) { singleValueMap.put(key, values.get(0)); } }); diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java index b4ecbc5afa54..8657738dc623 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -16,26 +16,16 @@ package org.springframework.util; -import java.io.File; import java.lang.reflect.Array; -import java.net.InetAddress; -import java.net.URI; -import java.net.URL; import java.nio.charset.Charset; -import java.nio.file.Path; import java.time.ZoneId; -import java.time.temporal.Temporal; import java.util.Arrays; import java.util.Collection; -import java.util.Currency; -import java.util.Date; -import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.StringJoiner; import java.util.TimeZone; -import java.util.UUID; -import java.util.regex.Pattern; import org.springframework.lang.Nullable; @@ -59,9 +49,6 @@ */ public abstract class ObjectUtils { - private static final int INITIAL_HASH = 7; - private static final int MULTIPLIER = 31; - private static final String EMPTY_STRING = ""; private static final String NULL_STRING = "null"; private static final String ARRAY_START = "{"; @@ -89,7 +76,7 @@ public static boolean isCheckedException(Throwable ex) { /** * Check whether the given exception is compatible with the specified - * exception types, as declared in a throws clause. + * exception types, as declared in a {@code throws} clause. * @param ex the exception to check * @param declaredExceptions the exception types declared in the throws clause * @return whether the given exception is compatible @@ -154,10 +141,10 @@ public static boolean isEmpty(@Nullable Object obj) { } if (obj instanceof Optional optional) { - return !optional.isPresent(); + return optional.isEmpty(); } if (obj instanceof CharSequence charSequence) { - return charSequence.length() == 0; + return charSequence.isEmpty(); } if (obj.getClass().isArray()) { return Array.getLength(obj) == 0; @@ -183,7 +170,7 @@ public static boolean isEmpty(@Nullable Object obj) { @Nullable public static Object unwrapOptional(@Nullable Object obj) { if (obj instanceof Optional optional) { - if (!optional.isPresent()) { + if (optional.isEmpty()) { return null; } Object result = optional.get(); @@ -255,7 +242,7 @@ public static > E caseInsensitiveValueOf(E[] enumValues, Strin } } throw new IllegalArgumentException("Constant [" + constant + "] does not exist in enum type " + - enumValues.getClass().getComponentType().getName()); + enumValues.getClass().componentType().getName()); } /** @@ -281,7 +268,7 @@ public static A[] addObjectToArray(@Nullable A[] array, @Nullab public static A[] addObjectToArray(@Nullable A[] array, @Nullable O obj, int position) { Class componentType = Object.class; if (array != null) { - componentType = array.getClass().getComponentType(); + componentType = array.getClass().componentType(); } else if (obj != null) { componentType = obj.getClass(); @@ -401,21 +388,32 @@ private static boolean arrayEquals(Object o1, Object o2) { } /** - * Return as hash code for the given object; typically the value of + * Return a hash code for the given elements, delegating to + * {@link #nullSafeHashCode(Object)} for each element. Contrary + * to {@link Objects#hash(Object...)}, this method can handle an + * element that is an array. + * @param elements the elements to be hashed + * @return a hash value of the elements + * @since 6.1 + */ + public static int nullSafeHash(@Nullable Object... elements) { + if (elements == null) { + return 0; + } + int result = 1; + for (Object element : elements) { + result = 31 * result + nullSafeHashCode(element); + } + return result; + } + + /** + * Return a hash code for the given object; typically the value of * {@code Object#hashCode()}}. If the object is an array, - * this method will delegate to any of the {@code nullSafeHashCode} - * methods for arrays in this class. If the object is {@code null}, - * this method returns 0. + * this method will delegate to any of the {@code Arrays.hashCode} + * methods. If the object is {@code null}, this method returns 0. * @see Object#hashCode() - * @see #nullSafeHashCode(Object[]) - * @see #nullSafeHashCode(boolean[]) - * @see #nullSafeHashCode(byte[]) - * @see #nullSafeHashCode(char[]) - * @see #nullSafeHashCode(double[]) - * @see #nullSafeHashCode(float[]) - * @see #nullSafeHashCode(int[]) - * @see #nullSafeHashCode(long[]) - * @see #nullSafeHashCode(short[]) + * @see Arrays */ public static int nullSafeHashCode(@Nullable Object obj) { if (obj == null) { @@ -423,31 +421,31 @@ public static int nullSafeHashCode(@Nullable Object obj) { } if (obj.getClass().isArray()) { if (obj instanceof Object[] objects) { - return nullSafeHashCode(objects); + return Arrays.hashCode(objects); } if (obj instanceof boolean[] booleans) { - return nullSafeHashCode(booleans); + return Arrays.hashCode(booleans); } if (obj instanceof byte[] bytes) { - return nullSafeHashCode(bytes); + return Arrays.hashCode(bytes); } if (obj instanceof char[] chars) { - return nullSafeHashCode(chars); + return Arrays.hashCode(chars); } if (obj instanceof double[] doubles) { - return nullSafeHashCode(doubles); + return Arrays.hashCode(doubles); } if (obj instanceof float[] floats) { - return nullSafeHashCode(floats); + return Arrays.hashCode(floats); } if (obj instanceof int[] ints) { - return nullSafeHashCode(ints); + return Arrays.hashCode(ints); } if (obj instanceof long[] longs) { - return nullSafeHashCode(longs); + return Arrays.hashCode(longs); } if (obj instanceof short[] shorts) { - return nullSafeHashCode(shorts); + return Arrays.hashCode(shorts); } } return obj.hashCode(); @@ -456,136 +454,91 @@ public static int nullSafeHashCode(@Nullable Object obj) { /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(Object[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable Object[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (Object element : array) { - hash = MULTIPLIER * hash + nullSafeHashCode(element); - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(boolean[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable boolean[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (boolean element : array) { - hash = MULTIPLIER * hash + Boolean.hashCode(element); - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(byte[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable byte[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (byte element : array) { - hash = MULTIPLIER * hash + element; - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(char[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable char[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (char element : array) { - hash = MULTIPLIER * hash + element; - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(double[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable double[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (double element : array) { - hash = MULTIPLIER * hash + Double.hashCode(element); - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(float[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable float[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (float element : array) { - hash = MULTIPLIER * hash + Float.hashCode(element); - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(int[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable int[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (int element : array) { - hash = MULTIPLIER * hash + element; - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(long[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable long[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (long element : array) { - hash = MULTIPLIER * hash + Long.hashCode(element); - } - return hash; + return Arrays.hashCode(array); } /** * Return a hash code based on the contents of the specified array. * If {@code array} is {@code null}, this method returns 0. + * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(short[])} */ + @Deprecated(since = "6.1") public static int nullSafeHashCode(@Nullable short[] array) { - if (array == null) { - return 0; - } - int hash = INITIAL_HASH; - for (short element : array) { - hash = MULTIPLIER * hash + element; - } - return hash; + return Arrays.hashCode(array); } @@ -932,14 +885,18 @@ public static String nullSafeToString(@Nullable short[] array) { * *

      In the context of this method, a simple value type is any of the following: * primitive wrapper (excluding {@link Void}), {@link Enum}, {@link Number}, - * {@link Date}, {@link Temporal}, {@link File}, {@link Path}, {@link URI}, - * {@link URL}, {@link InetAddress}, {@link Currency}, {@link Locale}, - * {@link UUID}, {@link Pattern}. + * {@link java.util.Date Date}, {@link java.time.temporal.Temporal Temporal}, + * {@link java.io.File File}, {@link java.nio.file.Path Path}, + * {@link java.net.URI URI}, {@link java.net.URL URL}, + * {@link java.net.InetAddress InetAddress}, {@link java.util.Currency Currency}, + * {@link java.util.Locale Locale}, {@link java.util.UUID UUID}, + * {@link java.util.regex.Pattern Pattern}. * @param obj the object to build a string representation for * @return a concise string representation of the supplied object * @since 5.3.27 * @see #nullSafeToString(Object) * @see StringUtils#truncate(CharSequence) + * @see ClassUtils#isSimpleValueType(Class) */ public static String nullSafeConciseToString(@Nullable Object obj) { if (obj == null) { @@ -974,7 +931,7 @@ public static String nullSafeConciseToString(@Nullable Object obj) { return StringUtils.truncate(charSequence); } Class type = obj.getClass(); - if (isSimpleValueType(type)) { + if (ClassUtils.isSimpleValueType(type)) { String str = obj.toString(); if (str != null) { return StringUtils.truncate(str); @@ -983,34 +940,4 @@ public static String nullSafeConciseToString(@Nullable Object obj) { return type.getTypeName() + "@" + getIdentityHexString(obj); } - /** - * Derived from {@link org.springframework.beans.BeanUtils#isSimpleValueType}. - *

      As of 5.3.28, considering {@link UUID} in addition to the bean-level check. - *

      As of 5.3.29, additionally considering {@link File}, {@link Path}, - * {@link InetAddress}, {@link Charset}, {@link Currency}, {@link TimeZone}, - * {@link ZoneId}, {@link Pattern}. - */ - private static boolean isSimpleValueType(Class type) { - return (Void.class != type && void.class != type && - (ClassUtils.isPrimitiveOrWrapper(type) || - Enum.class.isAssignableFrom(type) || - CharSequence.class.isAssignableFrom(type) || - Number.class.isAssignableFrom(type) || - Date.class.isAssignableFrom(type) || - Temporal.class.isAssignableFrom(type) || - ZoneId.class.isAssignableFrom(type) || - TimeZone.class.isAssignableFrom(type) || - File.class.isAssignableFrom(type) || - Path.class.isAssignableFrom(type) || - Charset.class.isAssignableFrom(type) || - Currency.class.isAssignableFrom(type) || - InetAddress.class.isAssignableFrom(type) || - URI.class == type || - URL.class == type || - UUID.class == type || - Locale.class == type || - Pattern.class == type || - Class.class == type)); - } - } diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 67871015cae2..9f050351f0b6 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ import org.springframework.lang.Nullable; /** - * Utility methods for simple pattern matching, in particular for - * Spring's typical "xxx*", "*xxx" and "*xxx*" pattern styles. + * Utility methods for simple pattern matching, in particular for Spring's typical + * {@code xxx*}, {@code *xxx}, {@code *xxx*}, and {@code xxx*yyy} pattern styles. * * @author Juergen Hoeller * @since 2.0 @@ -28,9 +28,10 @@ public abstract class PatternMatchUtils { /** - * Match a String against the given pattern, supporting the following simple - * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an - * arbitrary number of pattern parts), as well as direct equality. + * Match a String against the given pattern, supporting direct equality as + * well as the following simple pattern styles: {@code xxx*}, {@code *xxx}, + * {@code *xxx*}, and {@code xxx*yyy} (with an arbitrary number of pattern parts). + *

      Returns {@code false} if the supplied String or pattern is {@code null}. * @param pattern the pattern to match against * @param str the String to match * @return whether the String matches the given pattern @@ -73,14 +74,16 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str } /** - * Match a String against the given patterns, supporting the following simple - * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an - * arbitrary number of pattern parts), as well as direct equality. + * Match a String against the given patterns, supporting direct equality as + * well as the following simple pattern styles: {@code xxx*}, {@code *xxx}, + * {@code *xxx*}, and {@code xxx*yyy} (with an arbitrary number of pattern parts). + *

      Returns {@code false} if the supplied String is {@code null} or if the + * supplied patterns array is {@code null} or empty. * @param patterns the patterns to match against * @param str the String to match * @return whether the String matches any of the given patterns */ - public static boolean simpleMatch(@Nullable String[] patterns, String str) { + public static boolean simpleMatch(@Nullable String[] patterns, @Nullable String str) { if (patterns != null) { for (String pattern : patterns) { if (simpleMatch(pattern, str)) { diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index 0fdbb75d4766..4d1af9d2175d 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -137,7 +137,7 @@ public static void handleInvocationTargetException(InvocationTargetException ex) * @param ex the exception to rethrow * @throws RuntimeException the rethrown exception */ - public static void rethrowRuntimeException(Throwable ex) { + public static void rethrowRuntimeException(@Nullable Throwable ex) { if (ex instanceof RuntimeException runtimeException) { throw runtimeException; } @@ -158,7 +158,7 @@ public static void rethrowRuntimeException(Throwable ex) { * @param throwable the exception to rethrow * @throws Exception the rethrown exception (in case of a checked exception) */ - public static void rethrowException(Throwable throwable) throws Exception { + public static void rethrowException(@Nullable Throwable throwable) throws Exception { if (throwable instanceof Exception exception) { throw exception; } @@ -463,7 +463,7 @@ private static Method[] getDeclaredMethods(Class clazz, boolean defensive) { if (result == null) { try { Method[] declaredMethods = clazz.getDeclaredMethods(); - List defaultMethods = findConcreteMethodsOnInterfaces(clazz); + List defaultMethods = findDefaultMethodsOnInterfaces(clazz); if (defaultMethods != null) { result = new Method[declaredMethods.length + defaultMethods.size()]; System.arraycopy(declaredMethods, 0, result, 0, declaredMethods.length); @@ -487,15 +487,15 @@ private static Method[] getDeclaredMethods(Class clazz, boolean defensive) { } @Nullable - private static List findConcreteMethodsOnInterfaces(Class clazz) { + private static List findDefaultMethodsOnInterfaces(Class clazz) { List result = null; for (Class ifc : clazz.getInterfaces()) { - for (Method ifcMethod : ifc.getMethods()) { - if (!Modifier.isAbstract(ifcMethod.getModifiers())) { + for (Method method : ifc.getMethods()) { + if (method.isDefault()) { if (result == null) { result = new ArrayList<>(); } - result.add(ifcMethod); + result.add(method); } } } @@ -609,6 +609,31 @@ public static Field findField(Class clazz, @Nullable String name, @Nullable C return null; } + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with the + * supplied {@code name}. Searches all superclasses up to {@link Object}. + * @param clazz the class to introspect + * @param name the name of the field (with upper/lower case to be ignored) + * @return the corresponding Field object, or {@code null} if not found + * @since 6.1 + */ + @Nullable + public static Field findFieldIgnoreCase(Class clazz, String name) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(name, "Name must not be null"); + Class searchType = clazz; + while (Object.class != searchType && searchType != null) { + Field[] fields = getDeclaredFields(searchType); + for (Field field : fields) { + if (name.equalsIgnoreCase(field.getName())) { + return field; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + /** * Set the field represented by the supplied {@linkplain Field field object} on * the specified {@linkplain Object target object} to the specified {@code value}. diff --git a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java index af5b6746b960..ec75c898f9bd 100644 --- a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.File; import java.io.FileNotFoundException; +import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -100,6 +101,7 @@ public abstract class ResourceUtils { * @return whether the location qualifies as a URL * @see #CLASSPATH_URL_PREFIX * @see java.net.URL + * @see #toURL(String) */ public static boolean isUrl(@Nullable String resourceLocation) { if (resourceLocation == null) { @@ -125,6 +127,7 @@ public static boolean isUrl(@Nullable String resourceLocation) { * "classpath:" pseudo URL, a "file:" URL, or a plain file path * @return a corresponding URL object * @throws FileNotFoundException if the resource cannot be resolved to a URL + * @see #toURL(String) */ public static URL getURL(String resourceLocation) throws FileNotFoundException { Assert.notNull(resourceLocation, "Resource location must not be null"); @@ -165,6 +168,7 @@ public static URL getURL(String resourceLocation) throws FileNotFoundException { * @return a corresponding File object * @throws FileNotFoundException if the resource cannot be resolved to * a file in the file system + * @see #getFile(URL) */ public static File getFile(String resourceLocation) throws FileNotFoundException { Assert.notNull(resourceLocation, "Resource location must not be null"); @@ -196,6 +200,7 @@ public static File getFile(String resourceLocation) throws FileNotFoundException * @return a corresponding File object * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system + * @see #getFile(URL, String) */ public static File getFile(URL resourceUrl) throws FileNotFoundException { return getFile(resourceUrl, "URL"); @@ -236,6 +241,7 @@ public static File getFile(URL resourceUrl, String description) throws FileNotFo * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system * @since 2.5 + * @see #getFile(URI, String) */ public static File getFile(URI resourceUri) throws FileNotFoundException { return getFile(resourceUri, "URI"); @@ -267,6 +273,7 @@ public static File getFile(URI resourceUri, String description) throws FileNotFo * i.e. has protocol "file", "vfsfile" or "vfs". * @param url the URL to check * @return whether the URL has been identified as a file system URL + * @see #isJarURL(URL) */ public static boolean isFileURL(URL url) { String protocol = url.getProtocol(); @@ -275,10 +282,12 @@ public static boolean isFileURL(URL url) { } /** - * Determine whether the given URL points to a resource in a jar file. - * i.e. has protocol "jar", "war, ""zip", "vfszip" or "wsjar". + * Determine whether the given URL points to a resource in a jar file + * — for example, whether the URL has protocol "jar", "war, "zip", + * "vfszip", or "wsjar". * @param url the URL to check * @return whether the URL has been identified as a JAR URL + * @see #isJarFileURL(URL) */ public static boolean isJarURL(URL url) { String protocol = url.getProtocol(); @@ -293,6 +302,7 @@ public static boolean isJarURL(URL url) { * @param url the URL to check * @return whether the URL has been identified as a JAR file URL * @since 4.1 + * @see #extractJarFileURL(URL) */ public static boolean isJarFileURL(URL url) { return (URL_PROTOCOL_FILE.equals(url.getProtocol()) && @@ -305,6 +315,7 @@ public static boolean isJarFileURL(URL url) { * @param jarUrl the original URL * @return the URL for the actual jar file * @throws MalformedURLException if no valid jar file URL could be extracted + * @see #extractArchiveURL(URL) */ public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { String urlFile = jarUrl.getFile(); @@ -366,6 +377,7 @@ public static URL extractArchiveURL(URL jarUrl) throws MalformedURLException { * @return the URI instance * @throws URISyntaxException if the URL wasn't a valid URI * @see java.net.URL#toURI() + * @see #toURI(String) */ public static URI toURI(URL url) throws URISyntaxException { return toURI(url.toString()); @@ -377,64 +389,64 @@ public static URI toURI(URL url) throws URISyntaxException { * @param location the location String to convert into a URI instance * @return the URI instance * @throws URISyntaxException if the location wasn't a valid URI + * @see #toURI(URL) */ public static URI toURI(String location) throws URISyntaxException { return new URI(StringUtils.replace(location, " ", "%20")); } /** - * Create a URL instance for the given location String, + * Create a clean URL instance for the given location String, * going through URI construction and then URL conversion. * @param location the location String to convert into a URL instance * @return the URL instance * @throws MalformedURLException if the location wasn't a valid URL * @since 6.0 + * @see java.net.URI#toURL() + * @see #toURI(String) */ + @SuppressWarnings("deprecation") // on JDK 20 public static URL toURL(String location) throws MalformedURLException { - // Equivalent without java.net.URL constructor - for building on JDK 20+ - /* try { + // Prefer URI construction with toURL conversion (as of 6.1) return toURI(StringUtils.cleanPath(location)).toURL(); } catch (URISyntaxException | IllegalArgumentException ex) { - MalformedURLException exToThrow = new MalformedURLException(ex.getMessage()); - exToThrow.initCause(ex); - throw exToThrow; + // Lenient fallback to deprecated (on JDK 20) URL constructor, + // e.g. for decoded location Strings with percent characters. + return new URL(location); } - */ - - return new URL(location); } /** - * Create a URL instance for the given root URL and relative path, + * Create a clean URL instance for the given root URL and relative path, * going through URI construction and then URL conversion. * @param root the root URL to start from * @param relativePath the relative path to apply * @return the relative URL instance * @throws MalformedURLException if the end result is not a valid URL * @since 6.0 + * @see #toURL(String) + * @see StringUtils#applyRelativePath */ public static URL toRelativeURL(URL root, String relativePath) throws MalformedURLException { // # can appear in filenames, java.net.URL should not treat it as a fragment relativePath = StringUtils.replace(relativePath, "#", "%23"); - // Equivalent without java.net.URL constructor - for building on JDK 20+ - /* return toURL(StringUtils.applyRelativePath(root.toString(), relativePath)); - */ - - return new URL(root, relativePath); } /** * Set the {@link URLConnection#setUseCaches "useCaches"} flag on the - * given connection, preferring {@code false} but leaving the - * flag at {@code true} for JNLP based resources. + * given connection, preferring {@code false} but leaving the flag at + * its JVM default value for jar resources (typically {@code true}). * @param con the URLConnection to set the flag on + * @see URLConnection#setUseCaches */ public static void useCachesIfNecessary(URLConnection con) { - con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); + if (!(con instanceof JarURLConnection)) { + con.setUseCaches(false); + } } } diff --git a/spring-core/src/main/java/org/springframework/util/SerializationUtils.java b/spring-core/src/main/java/org/springframework/util/SerializationUtils.java index 22f18b7af085..1eb905a785f0 100644 --- a/spring-core/src/main/java/org/springframework/util/SerializationUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SerializationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,7 +99,9 @@ public static Object deserialize(@Nullable byte[] bytes) { */ @SuppressWarnings("unchecked") public static T clone(T object) { - return (T) SerializationUtils.deserialize(SerializationUtils.serialize(object)); + Object result = SerializationUtils.deserialize(SerializationUtils.serialize(object)); + Assert.state(result != null, "Deserialized object must not be null"); + return (T) result; } } diff --git a/spring-core/src/main/java/org/springframework/util/StopWatch.java b/spring-core/src/main/java/org/springframework/util/StopWatch.java index feecabbccfa0..1e2568978599 100644 --- a/spring-core/src/main/java/org/springframework/util/StopWatch.java +++ b/spring-core/src/main/java/org/springframework/util/StopWatch.java @@ -19,6 +19,7 @@ import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import org.springframework.lang.Nullable; @@ -37,12 +38,18 @@ * work and in development, rather than as part of production applications. * *

      As of Spring Framework 5.2, running time is tracked and reported in - * nanoseconds. + * nanoseconds. As of 6.1, the default time unit for String renderings is + * seconds with decimal points in nanosecond precision. Custom renderings with + * specific time units can be requested through {@link #prettyPrint(TimeUnit)}. * * @author Rod Johnson * @author Juergen Hoeller * @author Sam Brannen * @since May 2, 2001 + * @see #start() + * @see #stop() + * @see #shortSummary() + * @see #prettyPrint() */ public class StopWatch { @@ -53,9 +60,8 @@ public class StopWatch { */ private final String id; - private boolean keepTaskList = true; - - private final List taskList = new ArrayList<>(1); + @Nullable + private List taskList = new ArrayList<>(1); /** Start time of the current task. */ private long startTimeNanos; @@ -110,7 +116,7 @@ public String getId() { *

      Default is {@code true}. */ public void setKeepTaskList(boolean keepTaskList) { - this.keepTaskList = keepTaskList; + this.taskList = (keepTaskList ? new ArrayList<>() : null); } @@ -155,7 +161,7 @@ public void stop() throws IllegalStateException { long lastTime = System.nanoTime() - this.startTimeNanos; this.totalTimeNanos += lastTime; this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime); - if (this.keepTaskList) { + if (this.taskList != null) { this.taskList.add(this.lastTaskInfo); } ++this.taskCount; @@ -180,55 +186,78 @@ public String currentTaskName() { return this.currentTaskName; } + /** + * Get the last task as a {@link TaskInfo} object. + * @throws IllegalStateException if no tasks have run yet + * @since 6.1 + */ + public TaskInfo lastTaskInfo() throws IllegalStateException { + Assert.state(this.lastTaskInfo != null, "No tasks run"); + return this.lastTaskInfo; + } + + /** + * Get the last task as a {@link TaskInfo} object. + * @deprecated as of 6.1, in favor of {@link #lastTaskInfo()} + */ + @Deprecated(since = "6.1") + public TaskInfo getLastTaskInfo() throws IllegalStateException { + return lastTaskInfo(); + } + + /** + * Get the name of the last task. + * @see TaskInfo#getTaskName() + * @deprecated as of 6.1, in favor of {@link #lastTaskInfo()} + */ + @Deprecated(since = "6.1") + public String getLastTaskName() throws IllegalStateException { + return lastTaskInfo().getTaskName(); + } + /** * Get the time taken by the last task in nanoseconds. * @since 5.2 - * @see #getLastTaskTimeMillis() + * @see TaskInfo#getTimeNanos() + * @deprecated as of 6.1, in favor of {@link #lastTaskInfo()} */ + @Deprecated(since = "6.1") public long getLastTaskTimeNanos() throws IllegalStateException { - if (this.lastTaskInfo == null) { - throw new IllegalStateException("No tasks run: can't get last task interval"); - } - return this.lastTaskInfo.getTimeNanos(); + return lastTaskInfo().getTimeNanos(); } /** * Get the time taken by the last task in milliseconds. - * @see #getLastTaskTimeNanos() + * @see TaskInfo#getTimeMillis() + * @deprecated as of 6.1, in favor of {@link #lastTaskInfo()} */ + @Deprecated(since = "6.1") public long getLastTaskTimeMillis() throws IllegalStateException { - if (this.lastTaskInfo == null) { - throw new IllegalStateException("No tasks run: can't get last task interval"); - } - return this.lastTaskInfo.getTimeMillis(); + return lastTaskInfo().getTimeMillis(); } /** - * Get the name of the last task. + * Get an array of the data for tasks performed. + * @see #setKeepTaskList */ - public String getLastTaskName() throws IllegalStateException { - if (this.lastTaskInfo == null) { - throw new IllegalStateException("No tasks run: can't get last task name"); + public TaskInfo[] getTaskInfo() { + if (this.taskList == null) { + throw new UnsupportedOperationException("Task info is not being kept!"); } - return this.lastTaskInfo.getTaskName(); + return this.taskList.toArray(new TaskInfo[0]); } /** - * Get the last task as a {@link TaskInfo} object. + * Get the number of tasks timed. */ - public TaskInfo getLastTaskInfo() throws IllegalStateException { - if (this.lastTaskInfo == null) { - throw new IllegalStateException("No tasks run: can't get last task info"); - } - return this.lastTaskInfo; + public int getTaskCount() { + return this.taskCount; } - /** * Get the total time for all tasks in nanoseconds. * @since 5.2 - * @see #getTotalTimeMillis() - * @see #getTotalTimeSeconds() + * @see #getTotalTime(TimeUnit) */ public long getTotalTimeNanos() { return this.totalTimeNanos; @@ -236,89 +265,125 @@ public long getTotalTimeNanos() { /** * Get the total time for all tasks in milliseconds. - * @see #getTotalTimeNanos() - * @see #getTotalTimeSeconds() + * @see #getTotalTime(TimeUnit) */ public long getTotalTimeMillis() { - return nanosToMillis(this.totalTimeNanos); + return TimeUnit.NANOSECONDS.toMillis(this.totalTimeNanos); } /** * Get the total time for all tasks in seconds. - * @see #getTotalTimeNanos() - * @see #getTotalTimeMillis() + * @see #getTotalTime(TimeUnit) */ public double getTotalTimeSeconds() { - return nanosToSeconds(this.totalTimeNanos); + return getTotalTime(TimeUnit.SECONDS); } /** - * Get the number of tasks timed. - */ - public int getTaskCount() { - return this.taskCount; - } - - /** - * Get an array of the data for tasks performed. + * Get the total time for all tasks in the requested time unit + * (with decimal points in nanosecond precision). + * @param timeUnit the unit to use + * @since 6.1 + * @see #getTotalTimeNanos() + * @see #getTotalTimeMillis() + * @see #getTotalTimeSeconds() */ - public TaskInfo[] getTaskInfo() { - if (!this.keepTaskList) { - throw new UnsupportedOperationException("Task info is not being kept!"); - } - return this.taskList.toArray(new TaskInfo[0]); + public double getTotalTime(TimeUnit timeUnit) { + return (double) this.totalTimeNanos / TimeUnit.NANOSECONDS.convert(1, timeUnit); } /** - * Get a short description of the total running time. + * Generate a table describing all tasks performed in seconds + * (with decimal points in nanosecond precision). + *

      For custom reporting, call {@link #getTaskInfo()} and use the data directly. + * @see #prettyPrint(TimeUnit) + * @see #getTotalTimeSeconds() + * @see TaskInfo#getTimeSeconds() */ - public String shortSummary() { - return "StopWatch '" + getId() + "': running time = " + getTotalTimeNanos() + " ns"; + public String prettyPrint() { + return prettyPrint(TimeUnit.SECONDS); } /** - * Generate a string with a table describing all tasks performed. - *

      For custom reporting, call {@link #getTaskInfo()} and use the task info - * directly. + * Generate a table describing all tasks performed in the requested time unit + * (with decimal points in nanosecond precision). + *

      For custom reporting, call {@link #getTaskInfo()} and use the data directly. + * @param timeUnit the unit to use for rendering total time and task time + * @since 6.1 + * @see #prettyPrint() + * @see #getTotalTime(TimeUnit) + * @see TaskInfo#getTime(TimeUnit) */ - public String prettyPrint() { - StringBuilder sb = new StringBuilder(shortSummary()); - sb.append('\n'); - if (!this.keepTaskList) { - sb.append("No task info kept"); - } - else { - sb.append("---------------------------------------------\n"); - sb.append("ns % Task name\n"); - sb.append("---------------------------------------------\n"); - NumberFormat nf = NumberFormat.getNumberInstance(); - nf.setMinimumIntegerDigits(9); - nf.setGroupingUsed(false); - NumberFormat pf = NumberFormat.getPercentInstance(); - pf.setMinimumIntegerDigits(3); - pf.setGroupingUsed(false); - for (TaskInfo task : getTaskInfo()) { - sb.append(nf.format(task.getTimeNanos())).append(" "); - sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append(" "); + public String prettyPrint(TimeUnit timeUnit) { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.ENGLISH); + nf.setMaximumFractionDigits(9); + nf.setGroupingUsed(false); + + NumberFormat pf = NumberFormat.getPercentInstance(Locale.ENGLISH); + pf.setMinimumIntegerDigits(2); + pf.setGroupingUsed(false); + + StringBuilder sb = new StringBuilder(128); + sb.append("StopWatch '").append(getId()).append("': "); + String total = (timeUnit == TimeUnit.NANOSECONDS ? + nf.format(getTotalTimeNanos()) : nf.format(getTotalTime(timeUnit))); + sb.append(total).append(" ").append(timeUnit.name().toLowerCase(Locale.ENGLISH)); + int width = Math.max(sb.length(), 40); + sb.append("\n"); + + if (this.taskList != null) { + String line = "-".repeat(width) + "\n"; + String unitName = timeUnit.name(); + unitName = unitName.charAt(0) + unitName.substring(1).toLowerCase(Locale.ENGLISH); + unitName = String.format("%-12s", unitName); + sb.append(line); + sb.append(unitName).append(" % Task name\n"); + sb.append(line); + + int digits = total.indexOf('.'); + if (digits < 0) { + digits = total.length(); + } + nf.setMinimumIntegerDigits(digits); + nf.setMaximumFractionDigits(10 - digits); + + for (TaskInfo task : this.taskList) { + sb.append(String.format("%-14s", (timeUnit == TimeUnit.NANOSECONDS ? + nf.format(task.getTimeNanos()) : nf.format(task.getTime(timeUnit))))); + sb.append(String.format("%-8s", + pf.format(task.getTimeSeconds() / getTotalTimeSeconds()))); sb.append(task.getTaskName()).append('\n'); } } + else { + sb.append("No task info kept"); + } + return sb.toString(); } /** - * Generate an informative string describing all tasks performed - *

      For custom reporting, call {@link #getTaskInfo()} and use the task info - * directly. + * Get a short description of the total running time in seconds. + * @see #prettyPrint() + * @see #prettyPrint(TimeUnit) + */ + public String shortSummary() { + return "StopWatch '" + getId() + "': " + getTotalTimeSeconds() + " seconds"; + } + + /** + * Generate an informative string describing all tasks performed in seconds. + * @see #prettyPrint() + * @see #prettyPrint(TimeUnit) */ @Override public String toString() { StringBuilder sb = new StringBuilder(shortSummary()); - if (this.keepTaskList) { - for (TaskInfo task : getTaskInfo()) { - sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeNanos()).append(" ns"); - long percent = Math.round(100.0 * task.getTimeNanos() / getTotalTimeNanos()); + if (this.taskList != null) { + for (TaskInfo task : this.taskList) { + sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeSeconds()).append(" seconds"); + long percent = Math.round(100.0 * task.getTimeSeconds() / getTotalTimeSeconds()); sb.append(" = ").append(percent).append('%'); } } @@ -329,15 +394,6 @@ public String toString() { } - private static long nanosToMillis(long duration) { - return TimeUnit.NANOSECONDS.toMillis(duration); - } - - private static double nanosToSeconds(long duration) { - return duration / 1_000_000_000.0; - } - - /** * Nested class to hold data about one task executed within the {@code StopWatch}. */ @@ -362,8 +418,7 @@ public String getTaskName() { /** * Get the time this task took in nanoseconds. * @since 5.2 - * @see #getTimeMillis() - * @see #getTimeSeconds() + * @see #getTime(TimeUnit) */ public long getTimeNanos() { return this.timeNanos; @@ -371,20 +426,31 @@ public long getTimeNanos() { /** * Get the time this task took in milliseconds. - * @see #getTimeNanos() - * @see #getTimeSeconds() + * @see #getTime(TimeUnit) */ public long getTimeMillis() { - return nanosToMillis(this.timeNanos); + return TimeUnit.NANOSECONDS.toMillis(this.timeNanos); } /** * Get the time this task took in seconds. - * @see #getTimeMillis() - * @see #getTimeNanos() + * @see #getTime(TimeUnit) */ public double getTimeSeconds() { - return nanosToSeconds(this.timeNanos); + return getTime(TimeUnit.SECONDS); + } + + /** + * Get the time this task took in the requested time unit + * (with decimal points in nanosecond precision). + * @param timeUnit the unit to use + * @since 6.1 + * @see #getTimeNanos() + * @see #getTimeMillis() + * @see #getTimeSeconds() + */ + public double getTime(TimeUnit timeUnit) { + return (double) this.timeNanos / TimeUnit.NANOSECONDS.convert(1, timeUnit); } } diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java index 95368a137d93..1440a3fd5240 100644 --- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; import java.nio.charset.Charset; import org.springframework.lang.Nullable; @@ -133,9 +131,8 @@ public static void copy(String in, Charset charset, OutputStream out) throws IOE Assert.notNull(charset, "No Charset specified"); Assert.notNull(out, "No OutputStream specified"); - Writer writer = new OutputStreamWriter(out, charset); - writer.write(in); - writer.flush(); + out.write(in.getBytes(charset)); + out.flush(); } /** @@ -180,18 +177,13 @@ public static long copyRange(InputStream in, OutputStream out, long start, long long bytesToCopy = end - start + 1; byte[] buffer = new byte[(int) Math.min(StreamUtils.BUFFER_SIZE, bytesToCopy)]; while (bytesToCopy > 0) { - int bytesRead = in.read(buffer); + int bytesRead = (bytesToCopy < buffer.length ? in.read(buffer, 0, (int) bytesToCopy) : + in.read(buffer)); if (bytesRead == -1) { break; } - else if (bytesRead <= bytesToCopy) { - out.write(buffer, 0, bytesRead); - bytesToCopy -= bytesRead; - } - else { - out.write(buffer, 0, (int) bytesToCopy); - bytesToCopy = 0; - } + out.write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; } return (end - start + 1 - bytesToCopy); } @@ -204,8 +196,10 @@ else if (bytesRead <= bytesToCopy) { * @throws IOException in case of I/O errors * @since 4.3 */ - public static int drain(InputStream in) throws IOException { - Assert.notNull(in, "No InputStream specified"); + public static int drain(@Nullable InputStream in) throws IOException { + if (in == null) { + return 0; + } return (int) in.transferTo(OutputStream.nullOutputStream()); } diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 8936c605bab0..c72a55172a07 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,7 @@ * @author Arjen Poutsma * @author Sam Brannen * @author Brian Clozel + * @author Sebastien Deleuze * @since 16 April 2001 */ public abstract class StringUtils { @@ -70,6 +71,8 @@ public abstract class StringUtils { private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + private static final String DOUBLE_BACKSLASHES = "\\\\"; + private static final String TOP_PATH = ".."; private static final String CURRENT_PATH = "."; @@ -123,7 +126,7 @@ public static boolean isEmpty(@Nullable Object str) { * @see #hasText(CharSequence) */ public static boolean hasLength(@Nullable CharSequence str) { - return (str != null && str.length() > 0); + return (str != null && !str.isEmpty()); // as of JDK 15 } /** @@ -690,7 +693,7 @@ public static String applyRelativePath(String path, String relativePath) { * Normalize the path by suppressing sequences like "path/.." and * inner simple dots. *

      The result is convenient for path comparison. For other uses, - * notice that Windows separators ("\") are replaced by simple slashes. + * notice that Windows separators ("\" and "\\") are replaced by simple slashes. *

      NOTE that {@code cleanPath} should not be depended * upon in a security context. Other mechanisms should be used to prevent * path-traversal issues. @@ -702,7 +705,15 @@ public static String cleanPath(String path) { return path; } - String normalizedPath = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + String normalizedPath; + // Optimize when there is no backslash + if (path.indexOf('\\') != -1) { + normalizedPath = replace(path, DOUBLE_BACKSLASHES, FOLDER_SEPARATOR); + normalizedPath = replace(normalizedPath, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + } + else { + normalizedPath = path; + } String pathToUse = normalizedPath; // Shortcut if there is no work to do @@ -791,6 +802,7 @@ public static boolean pathEquals(String path1, String path2) { * and {@code "0"} through {@code "9"} stay the same. *

    3. Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
    4. *
    5. A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
    6. + *
    7. For all other characters (including those already decoded), the output is undefined.
    8. * * @param source the encoded String * @param charset the character set @@ -839,7 +851,7 @@ public static String uriDecode(String source, Charset charset) { * the {@link Locale#toString} format as well as BCP 47 language tags as * specified by {@link Locale#forLanguageTag}. * @param localeValue the locale value: following either {@code Locale's} - * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * {@code toString()} format ("en", "en_UK", etc.), also accepting spaces as * separators (as an alternative to underscores), or BCP 47 (e.g. "en-UK") * @return a corresponding {@code Locale} instance, or {@code null} if none * @throws IllegalArgumentException in case of an invalid locale specification @@ -852,7 +864,7 @@ public static Locale parseLocale(String localeValue) { if (!localeValue.contains("_") && !localeValue.contains(" ")) { validateLocalePart(localeValue); Locale resolved = Locale.forLanguageTag(localeValue); - if (resolved.getLanguage().length() > 0) { + if (!resolved.getLanguage().isEmpty()) { return resolved; } } @@ -868,7 +880,7 @@ public static Locale parseLocale(String localeValue) { *

      Note: This delegate does not accept the BCP 47 language tag format. * Please use {@link #parseLocale} for lenient parsing of both formats. * @param localeString the locale {@code String}: following {@code Locale's} - * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * {@code toString()} format ("en", "en_UK", etc.), also accepting spaces as * separators (as an alternative to underscores) * @return a corresponding {@code Locale} instance, or {@code null} if none * @throws IllegalArgumentException in case of an invalid locale specification @@ -876,7 +888,7 @@ public static Locale parseLocale(String localeValue) { @SuppressWarnings("deprecation") // for Locale constructors on JDK 19 @Nullable public static Locale parseLocaleString(String localeString) { - if (localeString.equals("")) { + if (localeString.isEmpty()) { return null; } @@ -1181,7 +1193,7 @@ public static String[] tokenizeToStringArray( if (trimTokens) { token = token.trim(); } - if (!ignoreEmptyTokens || token.length() > 0) { + if (!ignoreEmptyTokens || !token.isEmpty()) { tokens.add(token); } } @@ -1243,7 +1255,7 @@ public static String[] delimitedListToStringArray( result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); pos = delPos + delimiter.length(); } - if (str.length() > 0 && pos <= str.length()) { + if (!str.isEmpty() && pos <= str.length()) { // Add rest of String, but not in case of empty input. result.add(deleteAny(str.substring(pos), charsToDelete)); } diff --git a/spring-core/src/main/java/org/springframework/util/StringValueResolver.java b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java index 72ba1ec0ca41..1335de95867f 100644 --- a/spring-core/src/main/java/org/springframework/util/StringValueResolver.java +++ b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ * @since 2.5 * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#resolveAliases * @see org.springframework.beans.factory.config.BeanDefinitionVisitor#BeanDefinitionVisitor(StringValueResolver) - * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer */ @FunctionalInterface public interface StringValueResolver { diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java index 7c5ac6bdf425..21b58ca18eb6 100644 --- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,13 +35,13 @@ */ public abstract class SystemPropertyUtils { - /** Prefix for system property placeholders: "${". */ + /** Prefix for system property placeholders: {@value}. */ public static final String PLACEHOLDER_PREFIX = "${"; - /** Suffix for system property placeholders: "}". */ + /** Suffix for system property placeholders: {@value}. */ public static final String PLACEHOLDER_SUFFIX = "}"; - /** Value separator for system property placeholders: ":". */ + /** Value separator for system property placeholders: {@value}. */ public static final String VALUE_SEPARATOR = ":"; diff --git a/spring-core/src/main/java/org/springframework/util/TypeUtils.java b/spring-core/src/main/java/org/springframework/util/TypeUtils.java index 70e2e5a857cd..b118aa2eeef0 100644 --- a/spring-core/src/main/java/org/springframework/util/TypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/TypeUtils.java @@ -75,7 +75,7 @@ public static boolean isAssignable(Type lhsType, Type rhsType) { else if (lhsClass.isArray() && rhsType instanceof GenericArrayType rhsGenericArrayType) { Type rhsComponent = rhsGenericArrayType.getGenericComponentType(); - return isAssignable(lhsClass.getComponentType(), rhsComponent); + return isAssignable(lhsClass.componentType(), rhsComponent); } } @@ -97,7 +97,7 @@ else if (rhsType instanceof ParameterizedType rhsParameterizedType) { Type lhsComponent = lhsGenericArrayType.getGenericComponentType(); if (rhsType instanceof Class rhsClass && rhsClass.isArray()) { - return isAssignable(lhsComponent, rhsClass.getComponentType()); + return isAssignable(lhsComponent, rhsClass.componentType()); } else if (rhsType instanceof GenericArrayType rhsGenericArrayType) { Type rhsComponent = rhsGenericArrayType.getGenericComponentType(); diff --git a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java index 32fdeb1adae2..3d2f980575fd 100644 --- a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java @@ -47,7 +47,6 @@ final class UnmodifiableMultiValueMap implements MultiValueMap, Serial private static final long serialVersionUID = -8697084563854098920L; - @SuppressWarnings("serial") private final MultiValueMap delegate; @Nullable @@ -97,6 +96,7 @@ public List get(Object key) { } @Override + @Nullable public V getFirst(K key) { return this.delegate.getFirst(key); } @@ -266,7 +266,6 @@ private static class UnmodifiableEntrySet implements Set>> delegate; @SuppressWarnings("unchecked") @@ -516,7 +515,6 @@ private static class UnmodifiableValueCollection implements Collection> delegate; public UnmodifiableValueCollection(Collection> delegate) { diff --git a/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java index bd13e815ee47..129e014aee35 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,11 +33,10 @@ * else { * // sleep, e.g. Thread.sleep(waitInterval) * // retry operation - * } * } * * Once the underlying operation has completed successfully, - * the execution instance can be simply discarded. + * the execution instance can be discarded. * * @author Stephane Nicoll * @since 4.1 diff --git a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java index dceea17b91a5..5cd39685fa28 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.util.backoff; +import java.util.StringJoiner; + import org.springframework.util.Assert; /** @@ -44,11 +46,15 @@ * 10 30000 * * - *

      Note that the default max elapsed time is {@link Long#MAX_VALUE}. - * Use {@link #setMaxElapsedTime} to limit the maximum length of time that an - * instance should accumulate before returning {@link BackOffExecution#STOP}. + *

      Note that the default max elapsed time is {@link Long#MAX_VALUE}, and the + * default maximum number of attempts is {@link Integer#MAX_VALUE}. + * Use {@link #setMaxElapsedTime} to limit the length of time that an instance + * should accumulate before returning {@link BackOffExecution#STOP}. Alternatively, + * use {@link #setMaxAttempts} to limit the number of attempts. The execution + * stops when either of those two limits is reached. * * @author Stephane Nicoll + * @author Gary Russell * @since 4.1 */ public class ExponentialBackOff implements BackOff { @@ -73,6 +79,11 @@ public class ExponentialBackOff implements BackOff { */ public static final long DEFAULT_MAX_ELAPSED_TIME = Long.MAX_VALUE; + /** + * The default maximum attempts. + * @since 6.1 + */ + public static final int DEFAULT_MAX_ATTEMPTS = Integer.MAX_VALUE; private long initialInterval = DEFAULT_INITIAL_INTERVAL; @@ -82,6 +93,8 @@ public class ExponentialBackOff implements BackOff { private long maxElapsedTime = DEFAULT_MAX_ELAPSED_TIME; + private int maxAttempts = DEFAULT_MAX_ATTEMPTS; + /** * Create an instance with the default settings. @@ -89,6 +102,7 @@ public class ExponentialBackOff implements BackOff { * @see #DEFAULT_MULTIPLIER * @see #DEFAULT_MAX_INTERVAL * @see #DEFAULT_MAX_ELAPSED_TIME + * @see #DEFAULT_MAX_ATTEMPTS */ public ExponentialBackOff() { } @@ -151,6 +165,8 @@ public long getMaxInterval() { /** * Set the maximum elapsed time in milliseconds after which a call to * {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}. + * @param maxElapsedTime the maximum elapsed time + * @see #setMaxAttempts */ public void setMaxElapsedTime(long maxElapsedTime) { this.maxElapsedTime = maxElapsedTime; @@ -159,11 +175,35 @@ public void setMaxElapsedTime(long maxElapsedTime) { /** * Return the maximum elapsed time in milliseconds after which a call to * {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}. + * @return the maximum elapsed time + * @see #getMaxAttempts() */ public long getMaxElapsedTime() { return this.maxElapsedTime; } + /** + * The maximum number of attempts after which a call to + * {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}. + * @param maxAttempts the maximum number of attempts + * @since 6.1 + * @see #setMaxElapsedTime + */ + public void setMaxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; + } + + /** + * Return the maximum number of attempts after which a call to + * {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}. + * @return the maximum number of attempts + * @since 6.1 + * @see #getMaxElapsedTime() + */ + public int getMaxAttempts() { + return this.maxAttempts; + } + @Override public BackOffExecution start() { return new ExponentialBackOffExecution(); @@ -174,6 +214,16 @@ private void checkMultiplier(double multiplier) { "or equal to 1. A multiplier of 1 is equivalent to a fixed interval."); } + @Override + public String toString() { + return new StringJoiner(", ", ExponentialBackOff.class.getSimpleName() + "{", "}") + .add("initialInterval=" + this.initialInterval) + .add("multiplier=" + this.multiplier) + .add("maxInterval=" + this.maxInterval) + .add("maxElapsedTime=" + this.maxElapsedTime) + .add("maxAttempts=" + this.maxAttempts) + .toString(); + } private class ExponentialBackOffExecution implements BackOffExecution { @@ -181,13 +231,16 @@ private class ExponentialBackOffExecution implements BackOffExecution { private long currentElapsedTime = 0; + private int attempts; + @Override public long nextBackOff() { - if (this.currentElapsedTime >= getMaxElapsedTime()) { + if (this.currentElapsedTime >= getMaxElapsedTime() || this.attempts >= getMaxAttempts()) { return STOP; } long nextInterval = computeNextInterval(); this.currentElapsedTime += nextInterval; + this.attempts++; return nextInterval; } @@ -214,11 +267,12 @@ private long multiplyInterval(long maxInterval) { @Override public String toString() { - StringBuilder sb = new StringBuilder("ExponentialBackOff{"); - sb.append("currentInterval=").append(this.currentInterval < 0 ? "n/a" : this.currentInterval + "ms"); - sb.append(", multiplier=").append(getMultiplier()); - sb.append('}'); - return sb.toString(); + String currentIntervalDescription = this.currentInterval < 0 ? "n/a" : this.currentInterval + "ms"; + return new StringJoiner(", ", ExponentialBackOffExecution.class.getSimpleName() + "{", "}") + .add("currentInterval=" + currentIntervalDescription) + .add("multiplier=" + getMultiplier()) + .add("attempts=" + this.attempts) + .toString(); } } diff --git a/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java index 33a357912d03..6f65f92d5b72 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java @@ -26,6 +26,7 @@ * {@code true} or {@code false} first. * * @author Keith Donald + * @author Eugene Rabii * @since 1.2.2 */ @SuppressWarnings("serial") @@ -63,8 +64,9 @@ public BooleanComparator(boolean trueLow) { @Override - public int compare(Boolean v1, Boolean v2) { - return (v1 ^ v2) ? ((v1 ^ this.trueLow) ? 1 : -1) : 0; + public int compare(Boolean left, Boolean right) { + int multiplier = this.trueLow ? -1 : 1; + return multiplier * Boolean.compare(left, right); } @@ -75,7 +77,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return getClass().hashCode() * (this.trueLow ? -1 : 1); + return Boolean.hashCode(this.trueLow); } @Override diff --git a/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java index 07e7a9fed1f4..48a118928333 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,9 @@ * @since 1.2.2 * @param the type of comparable objects that may be compared by this comparator * @see Comparable + * @deprecated as of 6.1 in favor of {@link Comparator#naturalOrder()} */ +@Deprecated(since = "6.1") public class ComparableComparator> implements Comparator { /** diff --git a/spring-core/src/main/java/org/springframework/util/comparator/Comparators.java b/spring-core/src/main/java/org/springframework/util/comparator/Comparators.java index 543418fe5199..a061b498fb97 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/Comparators.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/Comparators.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,49 +29,47 @@ public abstract class Comparators { /** * Return a {@link Comparable} adapter. - * @see ComparableComparator#INSTANCE + * @see Comparator#naturalOrder() */ @SuppressWarnings("unchecked") public static Comparator comparable() { - return ComparableComparator.INSTANCE; + return (Comparator) Comparator.naturalOrder(); } /** * Return a {@link Comparable} adapter which accepts * null values and sorts them lower than non-null values. - * @see NullSafeComparator#NULLS_LOW + * @see Comparator#nullsFirst(Comparator) */ - @SuppressWarnings("unchecked") public static Comparator nullsLow() { - return NullSafeComparator.NULLS_LOW; + return nullsLow(comparable()); } /** * Return a decorator for the given comparator which accepts * null values and sorts them lower than non-null values. - * @see NullSafeComparator#NullSafeComparator(boolean) + * @see Comparator#nullsFirst(Comparator) */ public static Comparator nullsLow(Comparator comparator) { - return new NullSafeComparator<>(comparator, true); + return Comparator.nullsFirst(comparator); } /** * Return a {@link Comparable} adapter which accepts * null values and sorts them higher than non-null values. - * @see NullSafeComparator#NULLS_HIGH + * @see Comparator#nullsLast(Comparator) */ - @SuppressWarnings("unchecked") public static Comparator nullsHigh() { - return NullSafeComparator.NULLS_HIGH; + return nullsHigh(comparable()); } /** * Return a decorator for the given comparator which accepts * null values and sorts them higher than non-null values. - * @see NullSafeComparator#NullSafeComparator(boolean) + * @see Comparator#nullsLast(Comparator) */ public static Comparator nullsHigh(Comparator comparator) { - return new NullSafeComparator<>(comparator, false); + return Comparator.nullsLast(comparator); } } diff --git a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java index ab131ad7fe8c..77dce7608270 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java @@ -30,7 +30,10 @@ * @since 1.2.2 * @param the type of objects that may be compared by this comparator * @see Comparable + * @see Comparators + * @deprecated as of 6.1 in favor of {@link Comparator#nullsLast} and {@link Comparator#nullsFirst} */ +@Deprecated(since = "6.1") public class NullSafeComparator implements Comparator { /** @@ -69,9 +72,8 @@ public class NullSafeComparator implements Comparator { * @see #NULLS_LOW * @see #NULLS_HIGH */ - @SuppressWarnings("unchecked") private NullSafeComparator(boolean nullsLow) { - this.nonNullComparator = ComparableComparator.INSTANCE; + this.nonNullComparator = Comparators.comparable(); this.nullsLow = nullsLow; } @@ -92,17 +94,9 @@ public NullSafeComparator(Comparator comparator, boolean nullsLow) { @Override - public int compare(@Nullable T o1, @Nullable T o2) { - if (o1 == o2) { - return 0; - } - if (o1 == null) { - return (this.nullsLow ? -1 : 1); - } - if (o2 == null) { - return (this.nullsLow ? 1 : -1); - } - return this.nonNullComparator.compare(o1, o2); + public int compare(@Nullable T left, @Nullable T right) { + Comparator comparator = this.nullsLow ? Comparator.nullsFirst(this.nonNullComparator) : Comparator.nullsLast(this.nonNullComparator); + return comparator.compare(left, right); } @@ -115,7 +109,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return this.nonNullComparator.hashCode() * (this.nullsLow ? -1 : 1); + return Boolean.hashCode(this.nullsLow); } @Override diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java index a05e2436b0f0..7c12263be356 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public void addCallback(final ListenableFutureCallback callback) { @Override public void addCallback(final SuccessCallback successCallback, final FailureCallback failureCallback) { ListenableFuture listenableAdaptee = (ListenableFuture) getAdaptee(); - listenableAdaptee.addCallback(new ListenableFutureCallback() { + listenableAdaptee.addCallback(new ListenableFutureCallback<>() { @Override public void onSuccess(@Nullable S result) { T adapted = null; @@ -74,6 +74,7 @@ public void onSuccess(@Nullable S result) { } successCallback.onSuccess(adapted); } + @Override public void onFailure(Throwable ex) { failureCallback.onFailure(ex); diff --git a/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java index cf63e6c34fc7..9cd7f7b33f52 100644 --- a/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java +++ b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.util.function; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import org.springframework.lang.Nullable; @@ -31,6 +33,7 @@ * supplier for a method that returned {@code null} and caching the result. * * @author Juergen Hoeller + * @author Yanming Zhou * @since 5.1 * @param the type of results supplied by this supplier */ @@ -45,6 +48,11 @@ public class SingletonSupplier implements Supplier { @Nullable private volatile T singletonInstance; + /** + * Guards access to write operations on the {@code singletonInstance} field. + */ + private final Lock writeLock = new ReentrantLock(); + /** * Build a {@code SingletonSupplier} with the given singleton instance @@ -90,7 +98,8 @@ private SingletonSupplier(T singletonInstance) { public T get() { T instance = this.singletonInstance; if (instance == null) { - synchronized (this) { + this.writeLock.lock(); + try { instance = this.singletonInstance; if (instance == null) { if (this.instanceSupplier != null) { @@ -102,6 +111,9 @@ public T get() { this.singletonInstance = instance; } } + finally { + this.writeLock.unlock(); + } } return instance; } diff --git a/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java b/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java index 22f08636f277..af97e2fd99e0 100644 --- a/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java +++ b/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,4 +40,16 @@ public static T resolve(@Nullable Supplier supplier) { return (supplier != null ? supplier.get() : null); } + /** + * Resolve a given {@code Supplier}, getting its result or immediately + * returning the given Object as-is if not a {@code Supplier}. + * @param candidate the candidate to resolve (potentially a {@code Supplier}) + * @return a supplier's result or the given Object as-is + * @since 6.1.4 + */ + @Nullable + public static Object resolve(@Nullable Object candidate) { + return (candidate instanceof Supplier supplier ? supplier.get() : candidate); + } + } diff --git a/spring-core/src/main/java/org/springframework/util/unit/DataSize.java b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java index bc57d140dd9c..fa91266a4442 100644 --- a/spring-core/src/main/java/org/springframework/util/unit/DataSize.java +++ b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,7 +84,7 @@ private DataSize(long bytes) { /** * Obtain a {@link DataSize} representing the specified number of bytes. * @param bytes the number of bytes, positive or negative - * @return a {@link DataSize} + * @return a {@code DataSize} */ public static DataSize ofBytes(long bytes) { return new DataSize(bytes); @@ -93,7 +93,7 @@ public static DataSize ofBytes(long bytes) { /** * Obtain a {@link DataSize} representing the specified number of kilobytes. * @param kilobytes the number of kilobytes, positive or negative - * @return a {@link DataSize} + * @return a {@code DataSize} */ public static DataSize ofKilobytes(long kilobytes) { return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB)); @@ -102,7 +102,7 @@ public static DataSize ofKilobytes(long kilobytes) { /** * Obtain a {@link DataSize} representing the specified number of megabytes. * @param megabytes the number of megabytes, positive or negative - * @return a {@link DataSize} + * @return a {@code DataSize} */ public static DataSize ofMegabytes(long megabytes) { return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB)); @@ -111,7 +111,7 @@ public static DataSize ofMegabytes(long megabytes) { /** * Obtain a {@link DataSize} representing the specified number of gigabytes. * @param gigabytes the number of gigabytes, positive or negative - * @return a {@link DataSize} + * @return a {@code DataSize} */ public static DataSize ofGigabytes(long gigabytes) { return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB)); @@ -120,7 +120,7 @@ public static DataSize ofGigabytes(long gigabytes) { /** * Obtain a {@link DataSize} representing the specified number of terabytes. * @param terabytes the number of terabytes, positive or negative - * @return a {@link DataSize} + * @return a {@code DataSize} */ public static DataSize ofTerabytes(long terabytes) { return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB)); @@ -130,7 +130,7 @@ public static DataSize ofTerabytes(long terabytes) { * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}. * @param amount the amount of the size, measured in terms of the unit, * positive or negative - * @return a corresponding {@link DataSize} + * @return a corresponding {@code DataSize} */ public static DataSize of(long amount, DataUnit unit) { Assert.notNull(unit, "Unit must not be null"); @@ -140,15 +140,14 @@ public static DataSize of(long amount, DataUnit unit) { /** * Obtain a {@link DataSize} from a text string such as {@code 12MB} using * {@link DataUnit#BYTES} if no unit is specified. - *

      - * Examples: + *

      Examples: *

       	 * "12KB" -- parses as "12 kilobytes"
       	 * "5MB"  -- parses as "5 megabytes"
       	 * "20"   -- parses as "20 bytes"
       	 * 
      * @param text the text to parse - * @return the parsed {@link DataSize} + * @return the parsed {@code DataSize} * @see #parse(CharSequence, DataUnit) */ public static DataSize parse(CharSequence text) { @@ -158,26 +157,29 @@ public static DataSize parse(CharSequence text) { /** * Obtain a {@link DataSize} from a text string such as {@code 12MB} using * the specified default {@link DataUnit} if no unit is specified. - *

      - * The string starts with a number followed optionally by a unit matching one of the - * supported {@linkplain DataUnit suffixes}. - *

      - * Examples: + *

      The string starts with a number followed optionally by a unit matching + * one of the supported {@linkplain DataUnit suffixes}. + *

      If neither a unit nor a default {@code DataUnit} is specified, + * {@link DataUnit#BYTES} will be inferred. + *

      Examples: *

       	 * "12KB" -- parses as "12 kilobytes"
       	 * "5MB"  -- parses as "5 megabytes"
       	 * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
      +	 * "20"   -- parses as "20 bytes" (if the {@code defaultUnit} is {@code null})
       	 * 
      * @param text the text to parse - * @return the parsed {@link DataSize} + * @param defaultUnit the default {@code DataUnit} to use + * @return the parsed {@code DataSize} */ public static DataSize parse(CharSequence text, @Nullable DataUnit defaultUnit) { Assert.notNull(text, "Text must not be null"); try { - Matcher matcher = DataSizeUtils.PATTERN.matcher(StringUtils.trimAllWhitespace(text)); - Assert.state(matcher.matches(), "Does not match data size pattern"); + CharSequence trimmedText = StringUtils.trimAllWhitespace(text); + Matcher matcher = DataSizeUtils.PATTERN.matcher(trimmedText); + Assert.state(matcher.matches(), () -> "'" + text + "' does not match data size pattern"); DataUnit unit = DataSizeUtils.determineDataUnit(matcher.group(2), defaultUnit); - long amount = Long.parseLong(matcher.group(1)); + long amount = Long.parseLong(trimmedText, matcher.start(1), matcher.end(1), 10); return DataSize.of(amount, unit); } catch (Exception ex) { @@ -245,15 +247,15 @@ public String toString() { @Override - public boolean equals(@Nullable Object other) { - if (this == other) { + public boolean equals(@Nullable Object obj) { + if (this == obj) { return true; } - if (other == null || getClass() != other.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } - DataSize otherSize = (DataSize) other; - return (this.bytes == otherSize.bytes); + DataSize that = (DataSize) obj; + return (this.bytes == that.bytes); } @Override diff --git a/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java b/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java index 803139cd6ea1..488b70b47d91 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -112,24 +112,22 @@ public XMLEvent nextTag() throws XMLStreamException { while (true) { XMLEvent event = nextEvent(); switch (event.getEventType()) { - case XMLStreamConstants.START_ELEMENT: - case XMLStreamConstants.END_ELEMENT: + case XMLStreamConstants.START_ELEMENT, XMLStreamConstants.END_ELEMENT -> { return event; - case XMLStreamConstants.END_DOCUMENT: + } + case XMLStreamConstants.END_DOCUMENT -> { return null; - case XMLStreamConstants.SPACE: - case XMLStreamConstants.COMMENT: - case XMLStreamConstants.PROCESSING_INSTRUCTION: + } + case XMLStreamConstants.SPACE, XMLStreamConstants.COMMENT, XMLStreamConstants.PROCESSING_INSTRUCTION -> { continue; - case XMLStreamConstants.CDATA: - case XMLStreamConstants.CHARACTERS: + } + case XMLStreamConstants.CDATA, XMLStreamConstants.CHARACTERS -> { if (!event.asCharacters().isWhiteSpace()) { throw new XMLStreamException( "Non-ignorable whitespace CDATA or CHARACTERS event: " + event); } - break; - default: - throw new XMLStreamException("Expected START_ELEMENT or END_ELEMENT: " + event); + } + default -> throw new XMLStreamException("Expected START_ELEMENT or END_ELEMENT: " + event); } } } diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java index b895a4edb3af..4b8abba3903c 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceUri)) { } else { Set prefixes = this.namespaceUriToPrefixes.get(namespaceUri); - return (prefixes != null ? Collections.unmodifiableSet(prefixes) : Collections.emptySet()); + return (prefixes != null ? Collections.unmodifiableSet(prefixes) : Collections.emptySet()); } } diff --git a/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java b/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java index e6f6cd3d59f0..aaba75ba29f8 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java +++ b/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public static void enableIndenting(Transformer transformer) { *

      If the underlying XSLT engine is Xalan, then the special output key {@code indent-amount} * will be also be set to a value of {@link #DEFAULT_INDENT_AMOUNT} characters. * @param transformer the target transformer - * @param indentAmount the size of the indent (2 characters, 3 characters, etc) + * @param indentAmount the size of the indent (2 characters, 3 characters, etc.) * @see javax.xml.transform.Transformer#setOutputProperty(String, String) * @see javax.xml.transform.OutputKeys#INDENT */ diff --git a/spring-core/src/main/java21/org/springframework/core/task/VirtualThreadDelegate.java b/spring-core/src/main/java21/org/springframework/core/task/VirtualThreadDelegate.java new file mode 100644 index 000000000000..db94db9fa21d --- /dev/null +++ b/spring-core/src/main/java21/org/springframework/core/task/VirtualThreadDelegate.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task; + +import java.util.concurrent.ThreadFactory; + +/** + * Internal delegate for virtual thread handling on JDK 21. + * This is the actual version compiled against JDK 21. + * + * @author Juergen Hoeller + * @since 6.1 + * @see VirtualThreadTaskExecutor + */ +final class VirtualThreadDelegate { + + private final Thread.Builder threadBuilder = Thread.ofVirtual(); + + public ThreadFactory virtualThreadFactory() { + return this.threadBuilder.factory(); + } + + public ThreadFactory virtualThreadFactory(String threadNamePrefix) { + return this.threadBuilder.name(threadNamePrefix, 0).factory(); + } + + public Thread newVirtualThread(String name, Runnable task) { + return this.threadBuilder.name(name).unstarted(task); + } + +} diff --git a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt index e42228c717fd..5ac96350dfad 100644 --- a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt +++ b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,16 @@ operator fun PropertyResolver.get(key: String) : String? = getProperty(key) inline fun PropertyResolver.getProperty(key: String) : T? = getProperty(key, T::class.java) +/** + * Extension for [PropertyResolver.getProperty] providing a `getProperty(...)` + * variant returning a non-nullable `Foo` with a default value. + * + * @author John Burns + * @since 6.1 + */ +inline fun PropertyResolver.getProperty(key: String, default: T) : T = + getProperty(key, T::class.java, default) + /** * Extension for [PropertyResolver.getRequiredProperty] providing a * `getRequiredProperty(...)` variant. diff --git a/spring-core/src/main/resources/META-INF/spring/aot.factories b/spring-core/src/main/resources/META-INF/spring/aot.factories index 9d735f3b4240..6b32bb321cda 100644 --- a/spring-core/src/main/resources/META-INF/spring/aot.factories +++ b/spring-core/src/main/resources/META-INF/spring/aot.factories @@ -1,3 +1,6 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ +org.springframework.aot.hint.support.KotlinDetectorRuntimeHints,\ org.springframework.aot.hint.support.ObjectToObjectConverterRuntimeHints,\ -org.springframework.aot.hint.support.SpringFactoriesLoaderRuntimeHints +org.springframework.aot.hint.support.PathMatchingResourcePatternResolverRuntimeHints,\ +org.springframework.aot.hint.support.SpringFactoriesLoaderRuntimeHints,\ +org.springframework.aot.hint.support.SpringPropertiesRuntimeHints diff --git a/spring-core/src/test/java/a/ClassHavingNestedClass.java b/spring-core/src/test/java/a/ClassHavingNestedClass.java new file mode 100644 index 000000000000..2170cdcd860e --- /dev/null +++ b/spring-core/src/test/java/a/ClassHavingNestedClass.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package a; + +/** + * Test class for {@code org.springframework.util.ClassUtilsTests}. + * + *

      The use case for this test class requires that the package name is a single + * character (i.e., length of 1). + * + * @author Johnny Lim + */ +public class ClassHavingNestedClass { + + public static class NestedClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/DefaultMethodReferenceTests.java b/spring-core/src/test/java/org/springframework/aot/generate/DefaultMethodReferenceTests.java index d5c54e75abbe..bc62da2b9ba4 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/DefaultMethodReferenceTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/DefaultMethodReferenceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,7 +88,7 @@ void toCodeBlockWithStaticMethodRequiresDeclaringClass() { MethodSpec method = createTestMethod("methodName", new TypeName[0], Modifier.STATIC); MethodReference methodReference = new DefaultMethodReference(method, null); assertThatIllegalStateException().isThrownBy(methodReference::toCodeBlock) - .withMessage("static method reference must define a declaring class"); + .withMessage("Static method reference must define a declaring class"); } @Test diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassTests.java index 23802ed069f8..0c45a7a95bbe 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,6 +94,14 @@ void getOrAddWhenRepeatReturnsSameGeneratedClass() { assertThat(innerGeneratedClass).isSameAs(innerGeneratedClass2).isSameAs(innerGeneratedClass3); } + @Test + void generateJavaFileIsAnnotatedWithGenerated() { + GeneratedClass generatedClass = createGeneratedClass(TEST_CLASS_NAME); + assertThat(generatedClass.generateJavaFile().toString()) + .contains("@Generated") + .contains("import " + Generated.class.getName() + ";"); + } + @Test void generateJavaFileIncludesGeneratedMethods() { GeneratedClass generatedClass = createGeneratedClass(TEST_CLASS_NAME); diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java index bc10aa71a8a7..f13045242c5d 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java @@ -16,7 +16,6 @@ package org.springframework.aot.generate; -import java.io.IOException; import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -158,8 +157,7 @@ void getOrAddForFeatureComponentWhenHasFeatureNamePrefix() { } @Test - @SuppressWarnings("unchecked") - void writeToInvokeTypeSpecCustomizer() throws IOException { + void writeToInvokeTypeSpecCustomizer() { Consumer typeSpecCustomizer = mock(); this.generatedClasses.addForFeatureComponent("one", TestComponent.class, typeSpecCustomizer); verifyNoInteractions(typeSpecCustomizer); diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java index 0eeb510490e9..3372fe0c4dae 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java @@ -60,6 +60,15 @@ void addSourceFileWithJavaFileAddsFile() throws Exception { .contains("Hello, World!"); } + @Test + void addSourceFileWithJavaFileInTheDefaultPackageThrowsException() { + TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").build(); + JavaFile javaFile = JavaFile.builder("", helloWorld).build(); + assertThatIllegalArgumentException().isThrownBy(() -> this.generatedFiles.addSourceFile(javaFile)) + .withMessage("Could not add 'HelloWorld', processing classes in the " + + "default package is not supported. Did you forget to add a package statement?"); + } + @Test void addSourceFileWithCharSequenceAddsFile() throws Exception { this.generatedFiles.addSourceFile("com.example.HelloWorld", "{}"); @@ -73,6 +82,14 @@ void addSourceFileWithCharSequenceWhenClassNameIsEmptyThrowsException() { .withMessage("'className' must not be empty"); } + @Test + void addSourceFileWithCharSequenceWhenClassNameIsInTheDefaultPackageThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedFiles.addSourceFile("HelloWorld", "{}")) + .withMessage("Could not add 'HelloWorld', processing classes in the " + + "default package is not supported. Did you forget to add a package statement?"); + } + @Test void addSourceFileWithCharSequenceWhenClassNameIsInvalidThrowsException() { assertThatIllegalArgumentException() diff --git a/spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java index c8fc1ccfd4cf..c9150fa052cd 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ void addFileWhenFileAlreadyAddedThrowsException() { } @Test - void getGeneratedFilesReturnsFiles() throws Exception { + void getGeneratedFilesReturnsFiles() { this.generatedFiles.addResourceFile("META-INF/test1", "test1"); this.generatedFiles.addResourceFile("META-INF/test2", "test2"); assertThat(this.generatedFiles.getGeneratedFiles(Kind.RESOURCE)) diff --git a/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java new file mode 100644 index 000000000000..dced0ed7bbf1 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java @@ -0,0 +1,499 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.StringAssert; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.aot.generate.value.EnumWithClassBody; +import org.springframework.core.testfixture.aot.generate.value.ExampleClass; +import org.springframework.core.testfixture.aot.generate.value.ExampleClass$$GeneratedBy; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.FieldSpec; +import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.TypeSpec; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link ValueCodeGenerator}. + * + * @author Stephane Nicoll + */ +class ValueCodeGeneratorTests { + + + @Nested + class ConfigurationTests { + + @Test + void createWithListOfDelegatesInvokeThemInOrder() { + Delegate first = mock(Delegate.class); + Delegate second = mock(Delegate.class); + Delegate third = mock(Delegate.class); + ValueCodeGenerator codeGenerator = ValueCodeGenerator + .with(List.of(first, second, third)); + Object value = ""; + given(third.generateCode(codeGenerator, value)) + .willReturn(CodeBlock.of("test")); + CodeBlock code = codeGenerator.generateCode(value); + assertThat(code).hasToString("test"); + InOrder ordered = inOrder(first, second, third); + ordered.verify(first).generateCode(codeGenerator, value); + ordered.verify(second).generateCode(codeGenerator, value); + ordered.verify(third).generateCode(codeGenerator, value); + } + + @Test + void generateCodeWithMatchingDelegateStops() { + Delegate first = mock(Delegate.class); + Delegate second = mock(Delegate.class); + ValueCodeGenerator codeGenerator = ValueCodeGenerator + .with(List.of(first, second)); + Object value = ""; + given(first.generateCode(codeGenerator, value)) + .willReturn(CodeBlock.of("test")); + CodeBlock code = codeGenerator.generateCode(value); + assertThat(code).hasToString("test"); + verify(first).generateCode(codeGenerator, value); + verifyNoInteractions(second); + } + + @Test + void scopedReturnsImmutableCopy() { + ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + GeneratedMethods generatedMethods = new GeneratedMethods( + ClassName.get("com.example", "Test"), MethodName::toString); + ValueCodeGenerator scopedValueCodeGenerator = valueCodeGenerator.scoped(generatedMethods); + assertThat(scopedValueCodeGenerator).isNotSameAs(valueCodeGenerator); + assertThat(scopedValueCodeGenerator.getGeneratedMethods()).isSameAs(generatedMethods); + assertThat(valueCodeGenerator.getGeneratedMethods()).isNull(); + } + + } + + @Nested + class NullTests { + + @Test + void generateWhenNull() { + assertThat(generateCode(null)).hasToString("null"); + } + + } + + @Nested + class PrimitiveTests { + + @Test + void generateWhenBoolean() { + assertThat(generateCode(true)).hasToString("true"); + } + + @Test + void generateWhenByte() { + assertThat(generateCode((byte) 2)).hasToString("(byte) 2"); + } + + @Test + void generateWhenShort() { + assertThat(generateCode((short) 3)).hasToString("(short) 3"); + } + + @Test + void generateWhenInt() { + assertThat(generateCode(4)).hasToString("4"); + } + + @Test + void generateWhenLong() { + assertThat(generateCode(5L)).hasToString("5L"); + } + + @Test + void generateWhenFloat() { + assertThat(generateCode(0.1F)).hasToString("0.1F"); + } + + @Test + void generateWhenDouble() { + assertThat(generateCode(0.2)).hasToString("(double) 0.2"); + } + + @Test + void generateWhenChar() { + assertThat(generateCode('a')).hasToString("'a'"); + } + + @Test + void generateWhenSimpleEscapedCharReturnsEscaped() { + testEscaped('\b', "'\\b'"); + testEscaped('\t', "'\\t'"); + testEscaped('\n', "'\\n'"); + testEscaped('\f', "'\\f'"); + testEscaped('\r', "'\\r'"); + testEscaped('\"', "'\"'"); + testEscaped('\'', "'\\''"); + testEscaped('\\', "'\\\\'"); + } + + @Test + void generatedWhenUnicodeEscapedCharReturnsEscaped() { + testEscaped('\u007f', "'\\u007f'"); + } + + private void testEscaped(char value, String expectedSourceContent) { + assertThat(generateCode(value)).hasToString(expectedSourceContent); + } + + } + + @Nested + class StringTests { + + @Test + void generateWhenString() { + assertThat(generateCode("test")).hasToString("\"test\""); + } + + + @Test + void generateWhenStringWithCarriageReturn() { + assertThat(generateCode("test\n")).isEqualTo(CodeBlock.of("$S", "test\n")); + } + + } + + @Nested + class CharsetTests { + + @Test + void generateWhenCharset() { + assertThat(resolve(generateCode(StandardCharsets.UTF_8))).hasImport(Charset.class) + .hasValueCode("Charset.forName(\"UTF-8\")"); + } + + } + + @Nested + class EnumTests { + + @Test + void generateWhenEnum() { + assertThat(resolve(generateCode(ChronoUnit.DAYS))) + .hasImport(ChronoUnit.class).hasValueCode("ChronoUnit.DAYS"); + } + + @Test + void generateWhenEnumWithClassBody() { + assertThat(resolve(generateCode(EnumWithClassBody.TWO))) + .hasImport(EnumWithClassBody.class).hasValueCode("EnumWithClassBody.TWO"); + } + + } + + @Nested + class ClassTests { + + @Test + void generateWhenClass() { + assertThat(resolve(generateCode(InputStream.class))) + .hasImport(InputStream.class).hasValueCode("InputStream.class"); + } + + @Test + void generateWhenCglibClass() { + assertThat(resolve(generateCode(ExampleClass$$GeneratedBy.class))) + .hasImport(ExampleClass.class).hasValueCode("ExampleClass.class"); + } + + } + + @Nested + class ResolvableTypeTests { + + @Test + void generateWhenSimpleResolvableType() { + ResolvableType resolvableType = ResolvableType.forClass(String.class); + assertThat(resolve(generateCode(resolvableType))) + .hasImport(ResolvableType.class) + .hasValueCode("ResolvableType.forClass(String.class)"); + } + + @Test + void generateWhenNoneResolvableType() { + ResolvableType resolvableType = ResolvableType.NONE; + assertThat(resolve(generateCode(resolvableType))) + .hasImport(ResolvableType.class).hasValueCode("ResolvableType.NONE"); + } + + @Test + void generateWhenGenericResolvableType() { + ResolvableType resolvableType = ResolvableType + .forClassWithGenerics(List.class, String.class); + assertThat(resolve(generateCode(resolvableType))) + .hasImport(ResolvableType.class, List.class) + .hasValueCode("ResolvableType.forClassWithGenerics(List.class, String.class)"); + } + + @Test + void generateWhenNestedGenericResolvableType() { + ResolvableType stringList = ResolvableType.forClassWithGenerics(List.class, + String.class); + ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, + ResolvableType.forClass(Integer.class), stringList); + assertThat(resolve(generateCode(resolvableType))) + .hasImport(ResolvableType.class, List.class, Map.class).hasValueCode( + "ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(Integer.class), " + + "ResolvableType.forClassWithGenerics(List.class, String.class))"); + } + + } + + @Nested + class ArrayTests { + + @Test + void generateWhenPrimitiveArray() { + int[] array = { 0, 1, 2 }; + assertThat(generateCode(array)).hasToString("new int[] {0, 1, 2}"); + } + + @Test + void generateWhenWrapperArray() { + Integer[] array = { 0, 1, 2 }; + assertThat(resolve(generateCode(array))).hasValueCode("new Integer[] {0, 1, 2}"); + } + + @Test + void generateWhenClassArray() { + Class[] array = new Class[] { InputStream.class, OutputStream.class }; + assertThat(resolve(generateCode(array))).hasImport(InputStream.class, OutputStream.class) + .hasValueCode("new Class[] {InputStream.class, OutputStream.class}"); + } + + } + + @Nested + class ListTests { + + @Test + void generateWhenStringList() { + List list = List.of("a", "b", "c"); + assertThat(resolve(generateCode(list))).hasImport(List.class) + .hasValueCode("List.of(\"a\", \"b\", \"c\")"); + } + + @Test + void generateWhenEmptyList() { + List list = List.of(); + assertThat(resolve(generateCode(list))).hasImport(Collections.class) + .hasValueCode("Collections.emptyList()"); + } + + } + + @Nested + class SetTests { + + @Test + void generateWhenStringSet() { + Set set = Set.of("a", "b", "c"); + assertThat(resolve(generateCode(set))).hasImport(Set.class) + .hasValueCode("Set.of(\"a\", \"b\", \"c\")"); + } + + @Test + void generateWhenEmptySet() { + Set set = Set.of(); + assertThat(resolve(generateCode(set))).hasImport(Collections.class) + .hasValueCode("Collections.emptySet()"); + } + + @Test + void generateWhenLinkedHashSet() { + Set set = new LinkedHashSet<>(List.of("a", "b", "c")); + assertThat(resolve(generateCode(set))).hasImport(List.class, LinkedHashSet.class) + .hasValueCode("new LinkedHashSet(List.of(\"a\", \"b\", \"c\"))"); + } + + @Test + void generateWhenSetOfClass() { + Set> set = Set.of(InputStream.class, OutputStream.class); + assertThat(resolve(generateCode(set))).hasImport(Set.class, InputStream.class, OutputStream.class) + .valueCode().contains("Set.of(", "InputStream.class", "OutputStream.class"); + } + + } + + @Nested + class MapTests { + + @Test + void generateWhenSmallMap() { + Map map = Map.of("k1", "v1", "k2", "v2"); + assertThat(resolve(generateCode(map))).hasImport(Map.class) + .hasValueCode("Map.of(\"k1\", \"v1\", \"k2\", \"v2\")"); + } + + @Test + void generateWhenMapWithOverTenElements() { + Map map = new HashMap<>(); + for (int i = 1; i <= 11; i++) { + map.put("k" + i, "v" + i); + } + assertThat(resolve(generateCode(map))).hasImport(Map.class) + .valueCode().startsWith("Map.ofEntries("); + } + + } + + @Nested + class ExceptionTests { + + @Test + void generateWhenUnsupportedValue() { + StringWriter sw = new StringWriter(); + assertThatExceptionOfType(ValueCodeGenerationException.class) + .isThrownBy(() -> generateCode(sw)) + .withCauseInstanceOf(UnsupportedTypeValueCodeGenerationException.class) + .satisfies(ex -> assertThat(ex.getValue()).isEqualTo(sw)); + } + + @Test + void generateWhenUnsupportedDataTypeThrowsException() { + StringWriter sampleValue = new StringWriter(); + assertThatExceptionOfType(ValueCodeGenerationException.class).isThrownBy(() -> generateCode(sampleValue)) + .withMessageContaining("Failed to generate code for") + .withMessageContaining(sampleValue.toString()) + .withMessageContaining(StringWriter.class.getName()) + .havingCause() + .withMessageContaining("Code generation does not support") + .withMessageContaining(StringWriter.class.getName()); + } + + @Test + void generateWhenListOfUnsupportedElement() { + StringWriter one = new StringWriter(); + StringWriter two = new StringWriter(); + List list = List.of(one, two); + assertThatExceptionOfType(ValueCodeGenerationException.class).isThrownBy(() -> generateCode(list)) + .withMessageContaining("Failed to generate code for") + .withMessageContaining(list.toString()) + .withMessageContaining(list.getClass().getName()) + .havingCause() + .withMessageContaining("Failed to generate code for") + .withMessageContaining(one.toString()) + .withMessageContaining(StringWriter.class.getName()) + .havingCause() + .withMessageContaining("Code generation does not support " + StringWriter.class.getName()); + } + + } + + private static CodeBlock generateCode(@Nullable Object value) { + return ValueCodeGenerator.withDefaults().generateCode(value); + } + + private static ValueCode resolve(CodeBlock valueCode) { + String code = writeCode(valueCode); + List imports = code.lines() + .filter(candidate -> candidate.startsWith("import") && candidate.endsWith(";")) + .map(line -> line.substring("import".length(), line.length() - 1)) + .map(String::trim).toList(); + int start = code.indexOf("value = "); + int end = code.indexOf(";", start); + return new ValueCode(code.substring(start + "value = ".length(), end), imports); + } + + private static String writeCode(CodeBlock valueCode) { + FieldSpec field = FieldSpec.builder(Object.class, "value") + .initializer(valueCode) + .build(); + TypeSpec helloWorld = TypeSpec.classBuilder("Test").addField(field).build(); + JavaFile javaFile = JavaFile.builder("com.example", helloWorld).build(); + StringWriter out = new StringWriter(); + try { + javaFile.writeTo(out); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + return out.toString(); + } + + static class ValueCodeAssert extends AbstractAssert { + + public ValueCodeAssert(ValueCode actual) { + super(actual, ValueCodeAssert.class); + } + + ValueCodeAssert hasImport(Class... imports) { + for (Class anImport : imports) { + assertThat(this.actual.imports).contains(anImport.getName()); + } + return this; + } + + ValueCodeAssert hasValueCode(String code) { + assertThat(this.actual.code).isEqualTo(code); + return this; + } + + StringAssert valueCode() { + return new StringAssert(this.actual.code); + } + + } + + record ValueCode(String code, List imports) implements AssertProvider { + + @Override + public ValueCodeAssert assertThat() { + return new ValueCodeAssert(this); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java index d057197d9ff8..36cf25dfac54 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java @@ -16,14 +16,12 @@ package org.springframework.aot.hint; -import java.io.IOException; import java.lang.reflect.Type; import java.time.LocalDate; import java.util.List; import java.util.Set; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -229,8 +227,8 @@ void registerTypeForSerializationWithMultipleLevelsAndCollection() { @Test void registerTypeForSerializationWithEnum() { bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleEnum.class); - assertThat(this.hints.reflection().typeHints()).singleElement() - .satisfies(typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleEnum.class))); + assertThat(RuntimeHintsPredicates.reflection().onType(SampleEnum.class).withMemberCategories( + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)).accepts(this.hints); } @Test @@ -289,7 +287,8 @@ void registerTypeForJacksonCustomStrategy() { bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleRecordWithJacksonCustomStrategy.class); assertThat(RuntimeHintsPredicates.reflection().onType(PropertyNamingStrategies.UpperSnakeCaseStrategy.class).withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .accepts(this.hints); - assertThat(RuntimeHintsPredicates.reflection().onType(SampleRecordWithJacksonCustomStrategy.Builder.class).withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) + assertThat(RuntimeHintsPredicates.reflection().onType(SampleRecordWithJacksonCustomStrategy.Builder.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)) .accepts(this.hints); } @@ -443,7 +442,7 @@ public CustomDeserializer1() { } @Override - public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) { return null; } } @@ -456,7 +455,7 @@ public CustomDeserializer2() { } @Override - public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) { return null; } } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java index 591bcf953129..2c3351c752ca 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,6 @@ void registerTypeIfPresentRegistersExistingClass() { } @Test - @SuppressWarnings("unchecked") void registerTypeIfPresentIgnoresMissingClass() { Consumer hintBuilder = mock(); this.reflectionHints.registerTypeIfPresent(null, "com.example.DoesNotExist", hintBuilder); diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java index dd24398c300c..b8393dd52ba3 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -132,7 +132,6 @@ void registerIfPresentRegisterExistingLocation() { } @Test - @SuppressWarnings("unchecked") void registerIfPresentIgnoreMissingLocation() { Consumer hintBuilder = mock(); this.resourceHints.registerPatternIfPresent(null, "location/does-not-exist/", hintBuilder); diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java index f19689812ae0..f4b92183a1a1 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -224,7 +224,6 @@ public void notManaged() { @interface SampleInvoker { int retries() default 0; - } @Target({ ElementType.METHOD }) @@ -235,10 +234,9 @@ public void notManaged() { @AliasFor(attribute = "retries", annotation = SampleInvoker.class) int value() default 1; - } - @Target({ ElementType.TYPE }) + @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Reflective(TestTypeHintReflectiveProcessor.class) diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java index 37e65ce6ac31..38744bac6922 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ * * @author Sebastien Deleuze */ -public class RegisterReflectionForBindingProcessorTests { +class RegisterReflectionForBindingProcessorTests { private final RegisterReflectionForBindingProcessor processor = new RegisterReflectionForBindingProcessor(); @@ -59,7 +59,7 @@ void throwExceptionWithoutAnnotationAttributeOnClass() { } @Test - void throwExceptionWithoutAnnotationAttributeOnMethod() throws NoSuchMethodException { + void throwExceptionWithoutAnnotationAttributeOnMethod() { assertThatThrownBy(() -> processor.registerReflectionHints(hints.reflection(), SampleClassWithoutMethodLevelAnnotationAttribute.class.getMethod("method"))) .isInstanceOf(IllegalStateException.class); diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHintsTests.java new file mode 100644 index 000000000000..72965937c961 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHintsTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.hint.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link KotlinDetectorRuntimeHints}. + * @author Brian Clozel + */ +class KotlinDetectorRuntimeHintsTests { + + private RuntimeHints hints; + + @BeforeEach + void setup() { + this.hints = new RuntimeHints(); + SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories") + .load(RuntimeHintsRegistrar.class).forEach(registrar -> registrar + .registerHints(this.hints, ClassUtils.getDefaultClassLoader())); + } + + @Test + void kotlinMetadataHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(kotlin.Metadata.class)).accepts(this.hints); + } + + @Test + void kotlinReflectHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(kotlin.reflect.full.KClasses.class)).accepts(this.hints); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHintsTests.java new file mode 100644 index 000000000000..a62441493064 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHintsTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.hint.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PathMatchingResourcePatternResolverRuntimeHints}. + * + * @author Brian Clozel + */ +class PathMatchingResourcePatternResolverRuntimeHintsTests { + + private RuntimeHints hints; + + @BeforeEach + void setup() { + this.hints = new RuntimeHints(); + SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories") + .load(RuntimeHintsRegistrar.class).forEach(registrar -> registrar + .registerHints(this.hints, ClassUtils.getDefaultClassLoader())); + } + + @Test + void EclipseOsgiFileLocatorHasHints() { + assertThat(RuntimeHintsPredicates.reflection().onType(TypeReference.of("org.eclipse.core.runtime.FileLocator"))).accepts(this.hints); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHintsTests.java new file mode 100644 index 000000000000..4582b6f94ba8 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHintsTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.hint.support; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringPropertiesRuntimeHints}. + * @author Brian Clozel + */ +class SpringPropertiesRuntimeHintsTests { + + private RuntimeHints hints; + + @BeforeEach + void setup() { + this.hints = new RuntimeHints(); + SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories") + .load(RuntimeHintsRegistrar.class).forEach(registrar -> registrar + .registerHints(this.hints, ClassUtils.getDefaultClassLoader())); + } + + @Test + void springPropertiesResourceHasHints() { + assertThat(RuntimeHintsPredicates.resource().forResource("spring.properties")).accepts(this.hints); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java index d4a624f690fa..6a65db7e9d10 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.StringWriter; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import org.json.JSONException; import org.junit.jupiter.api.Test; @@ -32,8 +33,9 @@ * Tests for {@link ProxyHintsWriter}. * * @author Sebastien Deleuze + * @author Stephane Nicoll */ -public class ProxyHintsWriterTests { +class ProxyHintsWriterTests { @Test void empty() throws JSONException { @@ -63,6 +65,18 @@ void shouldWriteMultipleEntries() throws JSONException { ]""", hints); } + @Test + void shouldWriteEntriesInNaturalOrder() throws JSONException { + ProxyHints hints = new ProxyHints(); + hints.registerJdkProxy(Supplier.class); + hints.registerJdkProxy(Function.class); + assertEquals(""" + [ + { "interfaces": [ "java.util.function.Function" ] }, + { "interfaces": [ "java.util.function.Supplier" ] } + ]""", hints); + } + @Test void shouldWriteInnerClass() throws JSONException { ProxyHints hints = new ProxyHints(); @@ -88,7 +102,7 @@ private void assertEquals(String expectedString, ProxyHints hints) throws JSONEx StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); ProxyHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); } interface Inner { diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java index bb17e5a832ce..c9fb6901d792 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java @@ -33,12 +33,15 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.util.MimeType; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link ReflectionHintsWriter}. * * @author Sebastien Deleuze + * @author Stephane Nicoll */ -public class ReflectionHintsWriterTests { +class ReflectionHintsWriterTests { @Test void empty() throws JSONException { @@ -59,6 +62,7 @@ void one() throws JSONException { MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES) .withField("DEFAULT_CHARSET") .withField("defaultCharset") + .withField("aScore") .withConstructor(TypeReference.listOf(List.class, boolean.class, MimeType.class), ExecutableMode.INTROSPECT) .withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ExecutableMode.INVOKE) .withMethod("getDefaultCharset", Collections.emptyList(), ExecutableMode.INTROSPECT)); @@ -80,6 +84,7 @@ void one() throws JSONException { "allPublicClasses": true, "allDeclaredClasses": true, "fields": [ + { "name": "aScore" }, { "name": "DEFAULT_CHARSET" }, { "name": "defaultCharset" } ], @@ -203,17 +208,83 @@ void methodAndQueriedMethods() throws JSONException { @Test void ignoreLambda() throws JSONException { - Runnable anonymousRunnable = () -> { }; + Runnable anonymousRunnable = () -> {}; ReflectionHints hints = new ReflectionHints(); hints.registerType(anonymousRunnable.getClass()); assertEquals("[]", hints); } + @Test + void sortTypeHints() { + ReflectionHints hints = new ReflectionHints(); + hints.registerType(Integer.class, builder -> {}); + hints.registerType(Long.class, builder -> {}); + + ReflectionHints hints2 = new ReflectionHints(); + hints2.registerType(Long.class, builder -> {}); + hints2.registerType(Integer.class, builder -> {}); + + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortFieldHints() { + ReflectionHints hints = new ReflectionHints(); + hints.registerType(Integer.class, builder -> { + builder.withField("first"); + builder.withField("second"); + }); + ReflectionHints hints2 = new ReflectionHints(); + hints2.registerType(Integer.class, builder -> { + builder.withField("second"); + builder.withField("first"); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortConstructorHints() { + ReflectionHints hints = new ReflectionHints(); + hints.registerType(Integer.class, builder -> { + builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); + builder.withConstructor(List.of(TypeReference.of(String.class), + TypeReference.of(Integer.class)), ExecutableMode.INVOKE); + }); + + ReflectionHints hints2 = new ReflectionHints(); + hints2.registerType(Integer.class, builder -> { + builder.withConstructor(List.of(TypeReference.of(String.class), + TypeReference.of(Integer.class)), ExecutableMode.INVOKE); + builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortMethodHints() { + ReflectionHints hints = new ReflectionHints(); + hints.registerType(Integer.class, builder -> { + builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); + builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); + }); + + ReflectionHints hints2 = new ReflectionHints(); + hints2.registerType(Integer.class, builder -> { + builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); + builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + private void assertEquals(String expectedString, ReflectionHints hints) throws JSONException { + JSONAssert.assertEquals(expectedString, writeJson(hints), JSONCompareMode.STRICT); + } + + private String writeJson(ReflectionHints hints) { StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); ReflectionHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); + return out.toString(); } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java index a07a52191f07..b3fef587efa1 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,11 +49,11 @@ void registerExactMatch() throws JSONException { { "resources": { "includes": [ - { "pattern": "\\\\Qcom/example/test.properties\\\\E"}, { "pattern": "\\\\Q/\\\\E" }, { "pattern": "\\\\Qcom\\\\E"}, { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qcom/example/another.properties\\\\E"} + { "pattern": "\\\\Qcom/example/another.properties\\\\E"}, + { "pattern": "\\\\Qcom/example/test.properties\\\\E"} ] } }""", hints); @@ -82,10 +82,10 @@ void registerWildcardInTheMiddlePattern() throws JSONException { { "resources": { "includes": [ - { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, { "pattern": "\\\\Q/\\\\E" }, { "pattern": "\\\\Qcom\\\\E"}, - { "pattern": "\\\\Qcom/example\\\\E"} + { "pattern": "\\\\Qcom/example\\\\E"}, + { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"} ] } }""", hints); @@ -99,9 +99,9 @@ void registerWildcardAtTheEndPattern() throws JSONException { { "resources": { "includes": [ - { "pattern": "\\\\Qstatic/\\\\E.*"}, { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qstatic\\\\E"} + { "pattern": "\\\\Qstatic\\\\E"}, + { "pattern": "\\\\Qstatic/\\\\E.*"} ] } }""", hints); @@ -116,13 +116,13 @@ void registerPatternWithIncludesAndExcludes() throws JSONException { { "resources": { "includes": [ - { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, { "pattern": "\\\\Q/\\\\E"}, { "pattern": "\\\\Qcom\\\\E"}, { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"}, + { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, { "pattern": "\\\\Qorg\\\\E"}, - { "pattern": "\\\\Qorg/other\\\\E"} + { "pattern": "\\\\Qorg/other\\\\E"}, + { "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"} ], "excludes": [ { "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"}, @@ -140,10 +140,10 @@ void registerWithReachableTypeCondition() throws JSONException { { "resources": { "includes": [ - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"}, { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Q/\\\\E"}, { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom\\\\E"}, - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example\\\\E"} + { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example\\\\E"}, + { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"} ] } }""", hints); @@ -157,10 +157,10 @@ void registerType() throws JSONException { { "resources": { "includes": [ - { "pattern": "\\\\Qjava/lang/String.class\\\\E" }, { "pattern": "\\\\Q/\\\\E" }, { "pattern": "\\\\Qjava\\\\E" }, - { "pattern": "\\\\Qjava/lang\\\\E" } + { "pattern": "\\\\Qjava/lang\\\\E" }, + { "pattern": "\\\\Qjava/lang/String.class\\\\E" } ] } }""", hints); @@ -169,8 +169,8 @@ void registerType() throws JSONException { @Test void registerResourceBundle() throws JSONException { ResourceHints hints = new ResourceHints(); - hints.registerResourceBundle("com.example.message"); hints.registerResourceBundle("com.example.message2"); + hints.registerResourceBundle("com.example.message"); assertEquals(""" { "bundles": [ @@ -184,7 +184,7 @@ private void assertEquals(String expectedString, ResourceHints hints) throws JSO StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); ResourceHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java index b10d7a595f6d..bef492224894 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ * * @author Sebastien Deleuze */ -public class SerializationHintsWriterTests { +class SerializationHintsWriterTests { @Test void shouldWriteEmptyHint() throws JSONException { @@ -52,8 +52,8 @@ void shouldWriteSingleHint() throws JSONException { @Test void shouldWriteMultipleHints() throws JSONException { SerializationHints hints = new SerializationHints() - .registerType(TypeReference.of(String.class)) - .registerType(TypeReference.of(Environment.class)); + .registerType(TypeReference.of(Environment.class)) + .registerType(TypeReference.of(String.class)); assertEquals(""" [ { "name": "java.lang.String" }, @@ -75,7 +75,7 @@ private void assertEquals(String expectedString, SerializationHints hints) throw StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); SerializationHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); } } diff --git a/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java index 82a53c4e3f8e..78c703713293 100644 --- a/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java +++ b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link AttributeAccessorSupport}. + * Tests for {@link AttributeAccessorSupport}. * * @author Rob Harrop * @author Sam Brannen diff --git a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java index 00f9653d543a..b2bfb433296d 100644 --- a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ * @author Rob Harrop * @author Juergen Hoeller * @author Chris Beams + * @author Yanming Zhou */ @SuppressWarnings("rawtypes") class BridgeMethodResolverTests { @@ -86,6 +87,35 @@ void findBridgedMethodInHierarchy() throws Exception { assertThat(bridgedMethod.getParameterTypes()[0]).isEqualTo(Date.class); } + @Test + void findBridgedMethodFromOriginalMethodInHierarchy() throws Exception { + Method originalMethod = Adder.class.getMethod("add", Object.class); + assertThat(originalMethod.isBridge()).isFalse(); + Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(originalMethod, DateAdder.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + assertThat(bridgedMethod.getName()).isEqualTo("add"); + assertThat(bridgedMethod.getParameterCount()).isEqualTo(1); + assertThat(bridgedMethod.getParameterTypes()[0]).isEqualTo(Date.class); + } + + @Test + void findBridgedMethodFromOriginalMethodNotInHierarchy() throws Exception { + Method originalMethod = Adder.class.getMethod("add", Object.class); + Method mostSpecificMethod = BridgeMethodResolver.getMostSpecificMethod(originalMethod, FakeAdder.class); + assertThat(mostSpecificMethod).isSameAs(originalMethod); + } + + @Test + void findBridgedMethodInHierarchyWithBoundedGenerics() throws Exception { + Method originalMethod = Bar.class.getDeclaredMethod("someMethod", Object.class, Object.class); + assertThat(originalMethod.isBridge()).isFalse(); + Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(originalMethod, SubBar.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + assertThat(bridgedMethod.getName()).isEqualTo("someMethod"); + assertThat(bridgedMethod.getParameterCount()).isEqualTo(2); + assertThat(bridgedMethod.getParameterTypes()[0]).isEqualTo(CharSequence.class); + } + @Test void isBridgeMethodFor() throws Exception { Method bridged = MyBar.class.getDeclaredMethod("someMethod", String.class, Object.class); @@ -152,7 +182,7 @@ void withDoubleBoundParameterizedOnInstantiate() throws Exception { } @Test - void withGenericParameter() throws Exception { + void withGenericParameter() { Method[] methods = StringGenericParameter.class.getMethods(); Method bridgeMethod = null; Method bridgedMethod = null; @@ -173,7 +203,7 @@ void withGenericParameter() throws Exception { } @Test - void onAllMethods() throws Exception { + void onAllMethods() { Method[] methods = StringList.class.getMethods(); for (Method method : methods) { assertThat(BridgeMethodResolver.findBridgedMethod(method)).isNotNull(); @@ -206,7 +236,7 @@ void spr2603() throws Exception { } @Test - void spr2648() throws Exception { + void spr2648() { Method bridgeMethod = ReflectionUtils.findMethod(GenericSqlMapIntegerDao.class, "saveOrUpdate", Object.class); assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue(); Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(bridgeMethod); @@ -252,7 +282,7 @@ void spr3304() throws Exception { Method bridgedMethod = MegaMessageProducerImpl.class.getDeclaredMethod("receive", MegaMessageEvent.class); assertThat(bridgedMethod.isBridge()).isFalse(); - Method bridgeMethod = MegaMessageProducerImpl.class.getDeclaredMethod("receive", MegaEvent.class); + Method bridgeMethod = MegaMessageProducerImpl.class.getDeclaredMethod("receive", MegaEvent.class); assertThat(bridgeMethod.isBridge()).isTrue(); assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); @@ -296,7 +326,7 @@ void spr3485() throws Exception { } @Test - void spr3534() throws Exception { + void spr3534() { Method bridgeMethod = ReflectionUtils.findMethod(TestEmailProvider.class, "findBy", Object.class); assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue(); Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(bridgeMethod); @@ -346,7 +376,7 @@ public void someVarargMethod(String theArg, Object... otherArgs) { } - public static abstract class Bar { + public abstract static class Bar { void someMethod(Map m, Object otherArg) { } @@ -358,8 +388,15 @@ void someMethod(T theArg, Map m) { } - public static abstract class InterBar extends Bar { + public abstract static class InterBar extends Bar { + + @Override + void someMethod(T theArg, Object otherArg) { + } + } + + public abstract static class SubBar extends InterBar { } @@ -380,7 +417,7 @@ public interface Adder { } - public static abstract class AbstractDateAdder implements Adder { + public abstract static class AbstractDateAdder implements Adder { @Override public abstract void add(Date date); @@ -395,6 +432,13 @@ public void add(Date date) { } + public static class FakeAdder { + + public void add(Date date) { + } + } + + public static class Enclosing { public class Enclosed { @@ -475,7 +519,7 @@ public interface ConcreteSettingsDao extends SettingsDao implements Dao { + abstract static class AbstractDaoImpl implements Dao { protected T object; @@ -706,7 +750,7 @@ public interface UserInitiatedEvent { } - public static abstract class BaseUserInitiatedEvent extends GenericEvent implements UserInitiatedEvent { + public abstract static class BaseUserInitiatedEvent extends GenericEvent implements UserInitiatedEvent { } @@ -743,7 +787,7 @@ public static class GenericBroadcasterImpl implements Broadcaster { @SuppressWarnings({"unused", "unchecked"}) - public static abstract class GenericEventBroadcasterImpl + public abstract static class GenericEventBroadcasterImpl extends GenericBroadcasterImpl implements EventBroadcaster { private Class[] subscribingEvents; @@ -843,7 +887,7 @@ public void receive(ModifiedMessageEvent event) { public interface SimpleGenericRepository { - public Class getPersistentClass(); + Class getPersistentClass(); List findByQuery(); @@ -884,7 +928,7 @@ public SimpleGenericRepository getFor(Class entityType) { return null; } - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { } } @@ -1053,7 +1097,7 @@ public interface UserDao { } - public static abstract class AbstractDao { + public abstract static class AbstractDao { public void save(T t) { } @@ -1081,7 +1125,7 @@ public interface DaoInterface { } - public static abstract class BusinessGenericDao + public abstract static class BusinessGenericDao implements DaoInterface { public void save(T object) { @@ -1181,7 +1225,7 @@ public interface IGenericInterface { @SuppressWarnings("unused") - private static abstract class AbstractImplementsInterface implements IGenericInterface { + private abstract static class AbstractImplementsInterface implements IGenericInterface { @Override public void doSomething(D domainObject, T value) { @@ -1297,7 +1341,7 @@ public Collection findBy(EmailSearchConditions conditions) { // SPR-16103 classes //------------------- - public static abstract class BaseEntity { + public abstract static class BaseEntity { } public static class FooEntity extends BaseEntity { diff --git a/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java index e46ca45149ed..fd8810c15152 100644 --- a/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java +++ b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ import static org.springframework.core.CollectionFactory.createMap; /** - * Unit tests for {@link CollectionFactory}. + * Tests for {@link CollectionFactory}. * * @author Oliver Gierke * @author Sam Brannen @@ -212,6 +212,8 @@ void createsCollectionsCorrectly() { testCollection(List.class, ArrayList.class); testCollection(Set.class, LinkedHashSet.class); testCollection(Collection.class, LinkedHashSet.class); + // on JDK 21: testCollection(SequencedSet.class, LinkedHashSet.class); + // on JDK 21: testCollection(SequencedCollection.class, LinkedHashSet.class); testCollection(SortedSet.class, TreeSet.class); testCollection(NavigableSet.class, TreeSet.class); @@ -261,6 +263,7 @@ void rejectsNullCollectionType() { void createsMapsCorrectly() { // interfaces testMap(Map.class, LinkedHashMap.class); + // on JDK 21: testMap(SequencedMap.class, LinkedHashMap.class); testMap(SortedMap.class, TreeMap.class); testMap(NavigableMap.class, TreeMap.class); testMap(MultiValueMap.class, LinkedMultiValueMap.class); @@ -303,7 +306,7 @@ void rejectsNullMapType() { enum Color { - RED, BLUE; + RED, BLUE } } diff --git a/spring-core/src/test/java/org/springframework/core/ConstantsTests.java b/spring-core/src/test/java/org/springframework/core/ConstantsTests.java index 1c24092ff190..b39e353565d3 100644 --- a/spring-core/src/test/java/org/springframework/core/ConstantsTests.java +++ b/spring-core/src/test/java/org/springframework/core/ConstantsTests.java @@ -31,6 +31,7 @@ * @author Rick Evans * @since 28.04.2003 */ +@SuppressWarnings("deprecation") class ConstantsTests { @Test @@ -55,44 +56,44 @@ void constants() { void getNames() { Constants c = new Constants(A.class); - Set names = c.getNames(""); + Set names = c.getNames(""); assertThat(names).hasSize(c.getSize()); - assertThat(names.contains("DOG")).isTrue(); - assertThat(names.contains("CAT")).isTrue(); - assertThat(names.contains("S1")).isTrue(); + assertThat(names).contains("DOG"); + assertThat(names).contains("CAT"); + assertThat(names).contains("S1"); names = c.getNames("D"); assertThat(names).hasSize(1); - assertThat(names.contains("DOG")).isTrue(); + assertThat(names).contains("DOG"); names = c.getNames("d"); assertThat(names).hasSize(1); - assertThat(names.contains("DOG")).isTrue(); + assertThat(names).contains("DOG"); } @Test void getValues() { Constants c = new Constants(A.class); - Set values = c.getValues(""); + Set values = c.getValues(""); assertThat(values).hasSize(7); - assertThat(values.contains(0)).isTrue(); - assertThat(values.contains(66)).isTrue(); - assertThat(values.contains("")).isTrue(); + assertThat(values).contains(0); + assertThat(values).contains(66); + assertThat(values).contains(""); values = c.getValues("D"); assertThat(values).hasSize(1); - assertThat(values.contains(0)).isTrue(); + assertThat(values).contains(0); values = c.getValues("prefix"); assertThat(values).hasSize(2); - assertThat(values.contains(1)).isTrue(); - assertThat(values.contains(2)).isTrue(); + assertThat(values).contains(1); + assertThat(values).contains(2); values = c.getValuesForProperty("myProperty"); assertThat(values).hasSize(2); - assertThat(values.contains(1)).isTrue(); - assertThat(values.contains(2)).isTrue(); + assertThat(values).contains(1); + assertThat(values).contains(2); } @Test @@ -102,25 +103,25 @@ void getValuesInTurkey() { try { Constants c = new Constants(A.class); - Set values = c.getValues(""); + Set values = c.getValues(""); assertThat(values).hasSize(7); - assertThat(values.contains(0)).isTrue(); - assertThat(values.contains(66)).isTrue(); - assertThat(values.contains("")).isTrue(); + assertThat(values).contains(0); + assertThat(values).contains(66); + assertThat(values).contains(""); values = c.getValues("D"); assertThat(values).hasSize(1); - assertThat(values.contains(0)).isTrue(); + assertThat(values).contains(0); values = c.getValues("prefix"); assertThat(values).hasSize(2); - assertThat(values.contains(1)).isTrue(); - assertThat(values.contains(2)).isTrue(); + assertThat(values).contains(1); + assertThat(values).contains(2); values = c.getValuesForProperty("myProperty"); assertThat(values).hasSize(2); - assertThat(values.contains(1)).isTrue(); - assertThat(values.contains(2)).isTrue(); + assertThat(values).contains(1); + assertThat(values).contains(2); } finally { Locale.setDefault(oldLocale); @@ -131,15 +132,15 @@ void getValuesInTurkey() { void suffixAccess() { Constants c = new Constants(A.class); - Set names = c.getNamesForSuffix("_PROPERTY"); + Set names = c.getNamesForSuffix("_PROPERTY"); assertThat(names).hasSize(2); - assertThat(names.contains("NO_PROPERTY")).isTrue(); - assertThat(names.contains("YES_PROPERTY")).isTrue(); + assertThat(names).contains("NO_PROPERTY"); + assertThat(names).contains("YES_PROPERTY"); - Set values = c.getValuesForSuffix("_PROPERTY"); + Set values = c.getValuesForSuffix("_PROPERTY"); assertThat(values).hasSize(2); - assertThat(values.contains(3)).isTrue(); - assertThat(values.contains(4)).isTrue(); + assertThat(values).contains(3); + assertThat(values).contains(4); } @Test @@ -191,28 +192,28 @@ void toCode() { } @Test - void getValuesWithNullPrefix() throws Exception { + void getValuesWithNullPrefix() { Constants c = new Constants(A.class); Set values = c.getValues(null); assertThat(values).as("Must have returned *all* public static final values").hasSize(7); } @Test - void getValuesWithEmptyStringPrefix() throws Exception { + void getValuesWithEmptyStringPrefix() { Constants c = new Constants(A.class); Set values = c.getValues(""); assertThat(values).as("Must have returned *all* public static final values").hasSize(7); } @Test - void getValuesWithWhitespacedStringPrefix() throws Exception { + void getValuesWithWhitespacedStringPrefix() { Constants c = new Constants(A.class); Set values = c.getValues(" "); assertThat(values).as("Must have returned *all* public static final values").hasSize(7); } @Test - void withClassThatExposesNoConstants() throws Exception { + void withClassThatExposesNoConstants() { Constants c = new Constants(NoConstants.class); assertThat(c.getSize()).isEqualTo(0); final Set values = c.getValues(""); @@ -221,7 +222,7 @@ void withClassThatExposesNoConstants() throws Exception { } @Test - void ctorWithNullClass() throws Exception { + void ctorWithNullClass() { assertThatIllegalArgumentException().isThrownBy(() -> new Constants(null)); } diff --git a/spring-core/src/test/java/org/springframework/core/ConventionsTests.java b/spring-core/src/test/java/org/springframework/core/ConventionsTests.java index ada7e89b1ce8..7515feecac23 100644 --- a/spring-core/src/test/java/org/springframework/core/ConventionsTests.java +++ b/spring-core/src/test/java/org/springframework/core/ConventionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link Conventions}. + * Tests for {@link Conventions}. * * @author Rob Harrop * @author Sam Brannen diff --git a/spring-core/src/test/java/org/springframework/core/ExceptionDepthComparatorTests.java b/spring-core/src/test/java/org/springframework/core/ExceptionDepthComparatorTests.java index f30f04bf7873..3d243ea08822 100644 --- a/spring-core/src/test/java/org/springframework/core/ExceptionDepthComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/core/ExceptionDepthComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,55 +30,55 @@ class ExceptionDepthComparatorTests { @Test - void targetBeforeSameDepth() throws Exception { + void targetBeforeSameDepth() { Class foundClass = findClosestMatch(TargetException.class, SameDepthException.class); assertThat(foundClass).isEqualTo(TargetException.class); } @Test - void sameDepthBeforeTarget() throws Exception { + void sameDepthBeforeTarget() { Class foundClass = findClosestMatch(SameDepthException.class, TargetException.class); assertThat(foundClass).isEqualTo(TargetException.class); } @Test - void lowestDepthBeforeTarget() throws Exception { + void lowestDepthBeforeTarget() { Class foundClass = findClosestMatch(LowestDepthException.class, TargetException.class); assertThat(foundClass).isEqualTo(TargetException.class); } @Test - void targetBeforeLowestDepth() throws Exception { + void targetBeforeLowestDepth() { Class foundClass = findClosestMatch(TargetException.class, LowestDepthException.class); assertThat(foundClass).isEqualTo(TargetException.class); } @Test - void noDepthBeforeTarget() throws Exception { + void noDepthBeforeTarget() { Class foundClass = findClosestMatch(NoDepthException.class, TargetException.class); assertThat(foundClass).isEqualTo(TargetException.class); } @Test - void noDepthBeforeHighestDepth() throws Exception { + void noDepthBeforeHighestDepth() { Class foundClass = findClosestMatch(NoDepthException.class, HighestDepthException.class); assertThat(foundClass).isEqualTo(HighestDepthException.class); } @Test - void highestDepthBeforeNoDepth() throws Exception { + void highestDepthBeforeNoDepth() { Class foundClass = findClosestMatch(HighestDepthException.class, NoDepthException.class); assertThat(foundClass).isEqualTo(HighestDepthException.class); } @Test - void highestDepthBeforeLowestDepth() throws Exception { + void highestDepthBeforeLowestDepth() { Class foundClass = findClosestMatch(HighestDepthException.class, LowestDepthException.class); assertThat(foundClass).isEqualTo(LowestDepthException.class); } @Test - void lowestDepthBeforeHighestDepth() throws Exception { + void lowestDepthBeforeHighestDepth() { Class foundClass = findClosestMatch(LowestDepthException.class, HighestDepthException.class); assertThat(foundClass).isEqualTo(LowestDepthException.class); } diff --git a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java index 46e19190f4f5..3290133fd2d6 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,8 @@ /** * @author Juergen Hoeller * @author Sam Brannen + * @author Sebastien Deleuze + * @author Stephane Nicoll */ @SuppressWarnings({"unchecked", "rawtypes"}) class GenericTypeResolverTests { @@ -64,29 +66,30 @@ void simpleCollectionSuperclassType() { @Test void nullIfNotResolvable() { GenericClass obj = new GenericClass<>(); - assertThat((Object) resolveTypeArgument(obj.getClass(), GenericClass.class)).isNull(); + assertThat(resolveTypeArgument(obj.getClass(), GenericClass.class)).isNull(); } @Test void methodReturnTypes() { - assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "integer"), MyInterfaceType.class)).isEqualTo(Integer.class); - assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "string"), MyInterfaceType.class)).isEqualTo(String.class); - assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "raw"), MyInterfaceType.class)).isNull(); - assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class)).isNull(); + assertThat(resolveReturnTypeArgument(method(MyTypeWithMethods.class, "integer"), MyInterfaceType.class)).isEqualTo(Integer.class); + assertThat(resolveReturnTypeArgument(method(MyTypeWithMethods.class, "string"), MyInterfaceType.class)).isEqualTo(String.class); + assertThat(resolveReturnTypeArgument(method(MyTypeWithMethods.class, "character"), MyAbstractType.class)).isEqualTo(Character.class); + assertThat(resolveReturnTypeArgument(method(MyTypeWithMethods.class, "raw"), MyInterfaceType.class)).isNull(); + assertThat(resolveReturnTypeArgument(method(MyTypeWithMethods.class, "object"), MyInterfaceType.class)).isNull(); } @Test void testResolveType() { - Method intMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerInputMessage", MyInterfaceType.class); + Method intMessageMethod = method(MyTypeWithMethods.class, "readIntegerInputMessage", MyInterfaceType.class); MethodParameter intMessageMethodParam = new MethodParameter(intMessageMethod, 0); assertThat(resolveType(intMessageMethodParam.getGenericParameterType(), new HashMap<>())).isEqualTo(MyInterfaceType.class); - Method intArrMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerArrayInputMessage", + Method intArrMessageMethod = method(MyTypeWithMethods.class, "readIntegerArrayInputMessage", MyInterfaceType[].class); MethodParameter intArrMessageMethodParam = new MethodParameter(intArrMessageMethod, 0); assertThat(resolveType(intArrMessageMethodParam.getGenericParameterType(), new HashMap<>())).isEqualTo(MyInterfaceType[].class); - Method genericArrMessageMethod = findMethod(MySimpleTypeWithMethods.class, "readGenericArrayInputMessage", + Method genericArrMessageMethod = method(MySimpleTypeWithMethods.class, "readGenericArrayInputMessage", Object[].class); MethodParameter genericArrMessageMethodParam = new MethodParameter(genericArrMessageMethod, 0); Map varMap = getTypeVariableMap(MySimpleTypeWithMethods.class); @@ -99,7 +102,7 @@ void boundParameterizedType() { } @Test - void testGetTypeVariableMap() throws Exception { + void testGetTypeVariableMap() { Map map; map = GenericTypeResolver.getTypeVariableMap(MySimpleInterfaceType.class); @@ -136,16 +139,22 @@ void testGetTypeVariableMap() throws Exception { assertThat(x).isEqualTo(Long.class); } + @Test + void resolveTypeArgumentsOfAbstractType() { + Class[] resolved = GenericTypeResolver.resolveTypeArguments(MyConcreteType.class, MyAbstractType.class); + assertThat(resolved).containsExactly(Character.class); + } + @Test // SPR-11030 - void getGenericsCannotBeResolved() throws Exception { + void getGenericsCannotBeResolved() { Class[] resolved = GenericTypeResolver.resolveTypeArguments(List.class, Iterable.class); - assertThat((Object) resolved).isNull(); + assertThat(resolved).isNull(); } @Test // SPR-11052 - void getRawMapTypeCannotBeResolved() throws Exception { + void getRawMapTypeCannotBeResolved() { Class[] resolved = GenericTypeResolver.resolveTypeArguments(Map.class, Map.class); - assertThat((Object) resolved).isNull(); + assertThat(resolved).isNull(); } @Test // SPR-11044 @@ -174,17 +183,32 @@ void resolveIncompleteTypeVariables() { } @Test - public void resolvePartiallySpecializedTypeVariables() { + void resolvePartiallySpecializedTypeVariables() { Type resolved = resolveType(BiGenericClass.class.getTypeParameters()[0], TypeFixedBiGenericClass.class); assertThat(resolved).isEqualTo(D.class); } @Test - public void resolveTransitiveTypeVariableWithDifferentName() { + void resolveTransitiveTypeVariableWithDifferentName() { Type resolved = resolveType(BiGenericClass.class.getTypeParameters()[1], TypeFixedBiGenericClass.class); assertThat(resolved).isEqualTo(E.class); } + @Test + void resolveMethodParameterWithNestedGenerics() { + Method method = method(WithMethodParameter.class, "nestedGenerics", List.class); + MethodParameter methodParameter = new MethodParameter(method, 0); + Type resolvedType = resolveType(methodParameter.getGenericParameterType(), WithMethodParameter.class); + ParameterizedTypeReference>> reference = new ParameterizedTypeReference<>() {}; + assertThat(resolvedType).isEqualTo(reference.getType()); + } + + private static Method method(Class target, String methodName, Class... parameterTypes) { + Method method = findMethod(target, methodName, parameterTypes); + assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull(); + return method; + } + public interface MyInterfaceType { } @@ -194,6 +218,12 @@ public class MySimpleInterfaceType implements MyInterfaceType { public class MyCollectionInterfaceType implements MyInterfaceType> { } + public abstract class MyAbstractType implements MyInterfaceType { + } + + public class MyConcreteType extends MyAbstractType { + } + public abstract class MySuperclassType { } @@ -213,6 +243,8 @@ public MySimpleInterfaceType string() { return null; } + public MyConcreteType character() { return null; } + public Object object() { return null; } @@ -315,9 +347,9 @@ class TestIfc{} class TestImpl> extends TestIfc{ } - static abstract class BiGenericClass, V extends A> {} + abstract static class BiGenericClass, V extends A> {} - static abstract class SpecializedBiGenericClass extends BiGenericClass{} + abstract static class SpecializedBiGenericClass extends BiGenericClass{} static class TypeFixedBiGenericClass extends SpecializedBiGenericClass {} @@ -331,12 +363,12 @@ class TypedNested extends Nested { } } - static abstract class WithArrayBase { + abstract static class WithArrayBase { public abstract T[] array(T... args); } - static abstract class WithArray extends WithArrayBase { + abstract static class WithArray extends WithArrayBase { } interface Repository { @@ -345,4 +377,10 @@ interface Repository { interface IdFixingRepository extends Repository { } + static class WithMethodParameter { + public void nestedGenerics(List> input) { + } + } + + } diff --git a/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java b/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java deleted file mode 100644 index 5a00a7aa9df7..000000000000 --- a/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.core; - -import java.awt.Component; -import java.io.PrintStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.Date; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.tests.sample.objects.TestObject; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Adrian Colyer - */ -class LocalVariableTableParameterNameDiscovererTests { - - @SuppressWarnings("removal") - private final ParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); - - - @Test - void methodParameterNameDiscoveryNoArgs() throws NoSuchMethodException { - Method getName = TestObject.class.getMethod("getName"); - String[] names = discoverer.getParameterNames(getName); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("no argument names").isEmpty(); - } - - @Test - void methodParameterNameDiscoveryWithArgs() throws NoSuchMethodException { - Method setName = TestObject.class.getMethod("setName", String.class); - String[] names = discoverer.getParameterNames(setName); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("one argument").hasSize(1); - assertThat(names[0]).isEqualTo("name"); - } - - @Test - void consParameterNameDiscoveryNoArgs() throws NoSuchMethodException { - Constructor noArgsCons = TestObject.class.getConstructor(); - String[] names = discoverer.getParameterNames(noArgsCons); - assertThat(names).as("should find cons info").isNotNull(); - assertThat(names).as("no argument names").isEmpty(); - } - - @Test - void consParameterNameDiscoveryArgs() throws NoSuchMethodException { - Constructor twoArgCons = TestObject.class.getConstructor(String.class, int.class); - String[] names = discoverer.getParameterNames(twoArgCons); - assertThat(names).as("should find cons info").isNotNull(); - assertThat(names).as("one argument").hasSize(2); - assertThat(names[0]).isEqualTo("name"); - assertThat(names[1]).isEqualTo("age"); - } - - @Test - void staticMethodParameterNameDiscoveryNoArgs() throws NoSuchMethodException { - Method m = getClass().getMethod("staticMethodNoLocalVars"); - String[] names = discoverer.getParameterNames(m); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("no argument names").isEmpty(); - } - - @Test - void overloadedStaticMethod() throws Exception { - Class clazz = this.getClass(); - - Method m1 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE); - String[] names = discoverer.getParameterNames(m1); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("two arguments").hasSize(2); - assertThat(names[0]).isEqualTo("x"); - assertThat(names[1]).isEqualTo("y"); - - Method m2 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE, Long.TYPE); - names = discoverer.getParameterNames(m2); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("three arguments").hasSize(3); - assertThat(names[0]).isEqualTo("x"); - assertThat(names[1]).isEqualTo("y"); - assertThat(names[2]).isEqualTo("z"); - } - - @Test - void overloadedStaticMethodInInnerClass() throws Exception { - Class clazz = InnerClass.class; - - Method m1 = clazz.getMethod("staticMethod", Long.TYPE); - String[] names = discoverer.getParameterNames(m1); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("one argument").hasSize(1); - assertThat(names[0]).isEqualTo("x"); - - Method m2 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE); - names = discoverer.getParameterNames(m2); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("two arguments").hasSize(2); - assertThat(names[0]).isEqualTo("x"); - assertThat(names[1]).isEqualTo("y"); - } - - @Test - void overloadedMethod() throws Exception { - Class clazz = this.getClass(); - - Method m1 = clazz.getMethod("instanceMethod", Double.TYPE, Double.TYPE); - String[] names = discoverer.getParameterNames(m1); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("two arguments").hasSize(2); - assertThat(names[0]).isEqualTo("x"); - assertThat(names[1]).isEqualTo("y"); - - Method m2 = clazz.getMethod("instanceMethod", Double.TYPE, Double.TYPE, Double.TYPE); - names = discoverer.getParameterNames(m2); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("three arguments").hasSize(3); - assertThat(names[0]).isEqualTo("x"); - assertThat(names[1]).isEqualTo("y"); - assertThat(names[2]).isEqualTo("z"); - } - - @Test - void overloadedMethodInInnerClass() throws Exception { - Class clazz = InnerClass.class; - - Method m1 = clazz.getMethod("instanceMethod", String.class); - String[] names = discoverer.getParameterNames(m1); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("one argument").hasSize(1); - assertThat(names[0]).isEqualTo("aa"); - - Method m2 = clazz.getMethod("instanceMethod", String.class, String.class); - names = discoverer.getParameterNames(m2); - assertThat(names).as("should find method info").isNotNull(); - assertThat(names).as("two arguments").hasSize(2); - assertThat(names[0]).isEqualTo("aa"); - assertThat(names[1]).isEqualTo("bb"); - } - - @Test - void generifiedClass() throws Exception { - Class clazz = GenerifiedClass.class; - - Constructor ctor = clazz.getDeclaredConstructor(Object.class); - String[] names = discoverer.getParameterNames(ctor); - assertThat(names).hasSize(1); - assertThat(names[0]).isEqualTo("key"); - - ctor = clazz.getDeclaredConstructor(Object.class, Object.class); - names = discoverer.getParameterNames(ctor); - assertThat(names).hasSize(2); - assertThat(names[0]).isEqualTo("key"); - assertThat(names[1]).isEqualTo("value"); - - Method m = clazz.getMethod("generifiedStaticMethod", Object.class); - names = discoverer.getParameterNames(m); - assertThat(names).hasSize(1); - assertThat(names[0]).isEqualTo("param"); - - m = clazz.getMethod("generifiedMethod", Object.class, long.class, Object.class, Object.class); - names = discoverer.getParameterNames(m); - assertThat(names).hasSize(4); - assertThat(names[0]).isEqualTo("param"); - assertThat(names[1]).isEqualTo("x"); - assertThat(names[2]).isEqualTo("key"); - assertThat(names[3]).isEqualTo("value"); - - m = clazz.getMethod("voidStaticMethod", Object.class, long.class, int.class); - names = discoverer.getParameterNames(m); - assertThat(names).hasSize(3); - assertThat(names[0]).isEqualTo("obj"); - assertThat(names[1]).isEqualTo("x"); - assertThat(names[2]).isEqualTo("i"); - - m = clazz.getMethod("nonVoidStaticMethod", Object.class, long.class, int.class); - names = discoverer.getParameterNames(m); - assertThat(names).hasSize(3); - assertThat(names[0]).isEqualTo("obj"); - assertThat(names[1]).isEqualTo("x"); - assertThat(names[2]).isEqualTo("i"); - - m = clazz.getMethod("getDate"); - names = discoverer.getParameterNames(m); - assertThat(names).isEmpty(); - } - - @Disabled("Ignored because Ubuntu packages OpenJDK with debug symbols enabled. See SPR-8078.") - @Test - void classesWithoutDebugSymbols() throws Exception { - // JDK classes don't have debug information (usually) - Class clazz = Component.class; - String methodName = "list"; - - Method m = clazz.getMethod(methodName); - String[] names = discoverer.getParameterNames(m); - assertThat(names).isNull(); - - m = clazz.getMethod(methodName, PrintStream.class); - names = discoverer.getParameterNames(m); - assertThat(names).isNull(); - - m = clazz.getMethod(methodName, PrintStream.class, int.class); - names = discoverer.getParameterNames(m); - assertThat(names).isNull(); - } - - - public static void staticMethodNoLocalVars() { - } - - public static long staticMethod(long x, long y) { - long u = x * y; - return u; - } - - public static long staticMethod(long x, long y, long z) { - long u = x * y * z; - return u; - } - - public double instanceMethod(double x, double y) { - double u = x * y; - return u; - } - - public double instanceMethod(double x, double y, double z) { - double u = x * y * z; - return u; - } - - - public static class InnerClass { - - public int waz = 0; - - public InnerClass() { - } - - public InnerClass(String firstArg, long secondArg, Object thirdArg) { - long foo = 0; - short bar = 10; - this.waz = (int) (foo + bar); - } - - public String instanceMethod(String aa) { - return aa; - } - - public String instanceMethod(String aa, String bb) { - return aa + bb; - } - - public static long staticMethod(long x) { - long u = x; - return u; - } - - public static long staticMethod(long x, long y) { - long u = x * y; - return u; - } - } - - - public static class GenerifiedClass { - - private static long date; - - static { - // some custom static bloc or - date = new Date().getTime(); - } - - public GenerifiedClass() { - this(null, null); - } - - public GenerifiedClass(K key) { - this(key, null); - } - - public GenerifiedClass(K key, V value) { - } - - public static

      long generifiedStaticMethod(P param) { - return date; - } - - public

      void generifiedMethod(P param, long x, K key, V value) { - // nothing - } - - public static void voidStaticMethod(Object obj, long x, int i) { - // nothing - } - - public static long nonVoidStaticMethod(Object obj, long x, int i) { - return date; - } - - public static long getDate() { - return date; - } - } - -} diff --git a/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java new file mode 100644 index 000000000000..f11f8eb86598 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodIntrospector.MetadataLookup; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY; + +/** + * Tests for {@link MethodIntrospector}. + * + * @author Sam Brannen + * @since 5.3.34 + */ +class MethodIntrospectorTests { + + @Test // gh-32586 + void selectMethodsAndClearDeclaredMethodsCacheBetweenInvocations() { + Class targetType = ActualController.class; + + // Preconditions for this use case. + assertThat(targetType).isPublic(); + assertThat(targetType.getSuperclass()).isPackagePrivate(); + + MetadataLookup metadataLookup = (MetadataLookup) method -> { + if (MergedAnnotations.from(method, TYPE_HIERARCHY).isPresent(Mapped.class)) { + return method.getName(); + } + return null; + }; + + // Start with a clean slate. + ReflectionUtils.clearCache(); + + // Round #1 + Map methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + + // Simulate ConfigurableApplicationContext#refresh() which clears the + // ReflectionUtils#declaredMethodsCache but NOT the BridgeMethodResolver#cache. + // As a consequence, ReflectionUtils.getDeclaredMethods(...) will return a + // new set of methods that are logically equivalent to but not identical + // to (in terms of object identity) any bridged methods cached in the + // BridgeMethodResolver cache. + ReflectionUtils.clearCache(); + + // Round #2 + methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface Mapped { + } + + interface Controller { + + void unmappedMethod(); + + @Mapped + void update(); + + @Mapped + void delete(); + } + + // Must NOT be public. + abstract static class AbstractController implements Controller { + + @Override + public void unmappedMethod() { + } + + @Override + public void delete() { + } + } + + // MUST be public. + public static class ActualController extends AbstractController { + + @Override + public void update() { + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java b/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java index 6f5220f2771e..e01f0275cea4 100644 --- a/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java +++ b/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ class MethodParameterTests { @BeforeEach void setup() throws NoSuchMethodException { - method = getClass().getMethod("method", String.class, Long.TYPE); + method = getClass().getMethod("method", String.class, long.class); stringParameter = new MethodParameter(method, 0); longParameter = new MethodParameter(method, 1); intReturnType = new MethodParameter(method, -1); @@ -65,14 +65,14 @@ void equals() throws NoSuchMethodException { assertThat(longParameter).isEqualTo(longParameter); assertThat(intReturnType).isEqualTo(intReturnType); - assertThat(stringParameter.equals(longParameter)).isFalse(); - assertThat(stringParameter.equals(intReturnType)).isFalse(); - assertThat(longParameter.equals(stringParameter)).isFalse(); - assertThat(longParameter.equals(intReturnType)).isFalse(); - assertThat(intReturnType.equals(stringParameter)).isFalse(); - assertThat(intReturnType.equals(longParameter)).isFalse(); + assertThat(stringParameter).isNotEqualTo(longParameter); + assertThat(stringParameter).isNotEqualTo(intReturnType); + assertThat(longParameter).isNotEqualTo(stringParameter); + assertThat(longParameter).isNotEqualTo(intReturnType); + assertThat(intReturnType).isNotEqualTo(stringParameter); + assertThat(intReturnType).isNotEqualTo(longParameter); - Method method = getClass().getMethod("method", String.class, Long.TYPE); + Method method = getClass().getMethod("method", String.class, long.class); MethodParameter methodParameter = new MethodParameter(method, 0); assertThat(methodParameter).isEqualTo(stringParameter); assertThat(stringParameter).isEqualTo(methodParameter); @@ -86,7 +86,7 @@ void testHashCode() throws NoSuchMethodException { assertThat(longParameter.hashCode()).isEqualTo(longParameter.hashCode()); assertThat(intReturnType.hashCode()).isEqualTo(intReturnType.hashCode()); - Method method = getClass().getMethod("method", String.class, Long.TYPE); + Method method = getClass().getMethod("method", String.class, long.class); MethodParameter methodParameter = new MethodParameter(method, 0); assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode()); assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode()); diff --git a/spring-core/src/test/java/org/springframework/core/OrderComparatorTests.java b/spring-core/src/test/java/org/springframework/core/OrderComparatorTests.java index cd67f213fded..607b6efb3b64 100644 --- a/spring-core/src/test/java/org/springframework/core/OrderComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/core/OrderComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for the {@link OrderComparator} class. + * Tests for {@link OrderComparator}. * * @author Rick Evans * @author Stephane Nicoll diff --git a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java index 87b750dfbc92..db129ea74f39 100644 --- a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java +++ b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link ReactiveAdapterRegistry}. + * Tests for {@link ReactiveAdapterRegistry}. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -44,7 +44,7 @@ @SuppressWarnings("unchecked") class ReactiveAdapterRegistryTests { - private static final Duration ONE_SECOND = Duration.ofSeconds(1); + private static final Duration FIVE_SECONDS = Duration.ofSeconds(5); private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); @@ -134,7 +134,7 @@ void toFlux() { Publisher source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); Object target = getAdapter(Flux.class).fromPublisher(source); assertThat(target).isInstanceOf(Flux.class); - assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); + assertThat(((Flux) target).collectList().block(FIVE_SECONDS)).isEqualTo(sequence); } @Test @@ -142,7 +142,7 @@ void toMono() { Publisher source = io.reactivex.rxjava3.core.Flowable.fromArray(1, 2, 3); Object target = getAdapter(Mono.class).fromPublisher(source); assertThat(target).isInstanceOf(Mono.class); - assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); + assertThat(((Mono) target).block(FIVE_SECONDS)).isEqualTo(Integer.valueOf(1)); } @Test @@ -152,7 +152,7 @@ void toFlowPublisher() { Object target = getAdapter(Flow.Publisher.class).fromPublisher(source); assertThat(target).isInstanceOf(Flow.Publisher.class); assertThat(JdkFlowAdapter.flowPublisherToFlux((Flow.Publisher) target) - .collectList().block(ONE_SECOND)).isEqualTo(sequence); + .collectList().block(FIVE_SECONDS)).isEqualTo(sequence); } @Test @@ -169,7 +169,7 @@ void fromCompletableFuture() { future.complete(1); Object target = getAdapter(CompletableFuture.class).toPublisher(future); assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); - assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); + assertThat(((Mono) target).block(FIVE_SECONDS)).isEqualTo(Integer.valueOf(1)); } } @@ -227,7 +227,7 @@ void fromFlowable() { Object source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Flowable.class).toPublisher(source); assertThat(target).as("Expected Flux Publisher: " + target.getClass().getName()).isInstanceOf(Flux.class); - assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); + assertThat(((Flux) target).collectList().block(FIVE_SECONDS)).isEqualTo(sequence); } @Test @@ -236,7 +236,7 @@ void fromObservable() { Object source = io.reactivex.rxjava3.core.Observable.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Observable.class).toPublisher(source); assertThat(target).as("Expected Flux Publisher: " + target.getClass().getName()).isInstanceOf(Flux.class); - assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); + assertThat(((Flux) target).collectList().block(FIVE_SECONDS)).isEqualTo(sequence); } @Test @@ -244,7 +244,7 @@ void fromSingle() { Object source = io.reactivex.rxjava3.core.Single.just(1); Object target = getAdapter(io.reactivex.rxjava3.core.Single.class).toPublisher(source); assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); - assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); + assertThat(((Mono) target).block(FIVE_SECONDS)).isEqualTo(Integer.valueOf(1)); } @Test @@ -252,7 +252,7 @@ void fromCompletable() { Object source = io.reactivex.rxjava3.core.Completable.complete(); Object target = getAdapter(io.reactivex.rxjava3.core.Completable.class).toPublisher(source); assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); - ((Mono) target).block(ONE_SECOND); + ((Mono) target).block(FIVE_SECONDS); } } @@ -289,7 +289,7 @@ void toUni() { Publisher source = Mono.just(1); Object target = getAdapter(Uni.class).fromPublisher(source); assertThat(target).isInstanceOf(Uni.class); - assertThat(((Uni) target).await().atMost(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); + assertThat(((Uni) target).await().atMost(FIVE_SECONDS)).isEqualTo(Integer.valueOf(1)); } @Test @@ -297,7 +297,7 @@ void fromUni() { Uni source = Uni.createFrom().item(1); Object target = getAdapter(Uni.class).toPublisher(source); assertThat(target).isInstanceOf(Mono.class); - assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); + assertThat(((Mono) target).block(FIVE_SECONDS)).isEqualTo(Integer.valueOf(1)); } @Test @@ -306,7 +306,7 @@ void toMulti() { Publisher source = Flux.fromIterable(sequence); Object target = getAdapter(Multi.class).fromPublisher(source); assertThat(target).isInstanceOf(Multi.class); - assertThat(((Multi) target).collect().asList().await().atMost(ONE_SECOND)).isEqualTo(sequence); + assertThat(((Multi) target).collect().asList().await().atMost(FIVE_SECONDS)).isEqualTo(sequence); } @Test @@ -315,7 +315,7 @@ void fromMulti() { Multi source = Multi.createFrom().iterable(sequence); Object target = getAdapter(Multi.class).toPublisher(source); assertThat(target).isInstanceOf(Flux.class); - assertThat(((Flux) target).blockLast(ONE_SECOND)).isEqualTo(Integer.valueOf(3)); + assertThat(((Flux) target).blockLast(FIVE_SECONDS)).isEqualTo(Integer.valueOf(3)); } } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index f4810cb29dae..7880ca35d3bd 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -76,7 +76,7 @@ class ResolvableTypeTests { @Test - void noneReturnValues() throws Exception { + void noneReturnValues() { ResolvableType none = ResolvableType.NONE; assertThat(none.as(Object.class)).isEqualTo(ResolvableType.NONE); assertThat(none.asCollection()).isEqualTo(ResolvableType.NONE); @@ -99,7 +99,7 @@ void noneReturnValues() throws Exception { } @Test - void forClass() throws Exception { + void forClass() { ResolvableType type = ResolvableType.forClass(ExtendsList.class); assertThat(type.getType()).isEqualTo(ExtendsList.class); assertThat(type.getRawClass()).isEqualTo(ExtendsList.class); @@ -108,7 +108,7 @@ void forClass() throws Exception { } @Test - void forClassWithNull() throws Exception { + void forClassWithNull() { ResolvableType type = ResolvableType.forClass(null); assertThat(type.getType()).isEqualTo(Object.class); assertThat(type.getRawClass()).isEqualTo(Object.class); @@ -117,25 +117,27 @@ void forClassWithNull() throws Exception { } @Test - void forRawClass() throws Exception { + void forRawClass() { ResolvableType type = ResolvableType.forRawClass(ExtendsList.class); assertThat(type.getType()).isEqualTo(ExtendsList.class); assertThat(type.getRawClass()).isEqualTo(ExtendsList.class); assertThat(type.isAssignableFrom(ExtendsList.class)).isTrue(); assertThat(type.isAssignableFrom(ArrayList.class)).isFalse(); + assertThat(type).isNotEqualTo(ResolvableType.forClass(ExtendsList.class)); } @Test - void forRawClassWithNull() throws Exception { + void forRawClassWithNull() { ResolvableType type = ResolvableType.forRawClass(null); assertThat(type.getType()).isEqualTo(Object.class); assertThat(type.getRawClass()).isEqualTo(Object.class); assertThat(type.isAssignableFrom(Object.class)).isTrue(); assertThat(type.isAssignableFrom(String.class)).isTrue(); + assertThat(type).isNotEqualTo(ResolvableType.forClass(null)); } @Test // gh-23321 - void forRawClassAssignableFromTypeVariable() throws Exception { + void forRawClassAssignableFromTypeVariable() { ResolvableType typeVariable = ResolvableType.forClass(ExtendsList.class).as(List.class).getGeneric(); ResolvableType raw = ResolvableType.forRawClass(CharSequence.class); assertThat(raw.resolve()).isEqualTo(CharSequence.class); @@ -147,26 +149,26 @@ void forRawClassAssignableFromTypeVariable() throws Exception { } @Test // gh-28776 - void forInstanceNull() throws Exception { + void forInstanceNull() { assertThat(ResolvableType.forInstance(null)).isEqualTo(ResolvableType.NONE); } @Test - void forInstanceNoProvider() throws Exception { + void forInstanceNoProvider() { ResolvableType type = ResolvableType.forInstance(new Object()); assertThat(type.getType()).isEqualTo(Object.class); assertThat(type.resolve()).isEqualTo(Object.class); } @Test - void forInstanceProvider() throws Exception { + void forInstanceProvider() { ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<>(String.class)); assertThat(type.getRawClass()).isEqualTo(MyGenericInterfaceType.class); assertThat(type.getGeneric().resolve()).isEqualTo(String.class); } @Test - void forInstanceProviderNull() throws Exception { + void forInstanceProviderNull() { ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType(null)); assertThat(type.getType()).isEqualTo(MyGenericInterfaceType.class); assertThat(type.resolve()).isEqualTo(MyGenericInterfaceType.class); @@ -198,7 +200,7 @@ void forPrivateField() throws Exception { } @Test - void forFieldMustNotBeNull() throws Exception { + void forFieldMustNotBeNull() { assertThatIllegalArgumentException() .isThrownBy(() -> ResolvableType.forField(null)) .withMessage("Field must not be null"); @@ -212,7 +214,7 @@ void forConstructorParameter() throws Exception { } @Test - void forConstructorParameterMustNotBeNull() throws Exception { + void forConstructorParameterMustNotBeNull() { assertThatIllegalArgumentException() .isThrownBy(() -> ResolvableType.forConstructorParameter(null, 0)) .withMessage("Constructor must not be null"); @@ -226,7 +228,7 @@ void forMethodParameterByIndex() throws Exception { } @Test - void forMethodParameterByIndexMustNotBeNull() throws Exception { + void forMethodParameterByIndexMustNotBeNull() { assertThatIllegalArgumentException() .isThrownBy(() -> ResolvableType.forMethodParameter(null, 0)) .withMessage("Method must not be null"); @@ -266,7 +268,7 @@ void forMethodParameterWithNestingAndLevels() throws Exception { } @Test - void forMethodParameterMustNotBeNull() throws Exception { + void forMethodParameterMustNotBeNull() { assertThatIllegalArgumentException() .isThrownBy(() -> ResolvableType.forMethodParameter(null)) .withMessage("MethodParameter must not be null"); @@ -293,12 +295,27 @@ void forMethodReturn() throws Exception { } @Test - void forMethodReturnMustNotBeNull() throws Exception { + void forMethodReturnMustNotBeNull() { assertThatIllegalArgumentException() .isThrownBy(() -> ResolvableType.forMethodReturnType(null)) .withMessage("Method must not be null"); } + @Test // gh-27748 + void genericMatchesReturnType() throws Exception { + Method method = SomeRepository.class.getMethod("someMethod", Class.class, Class.class, Class.class); + + ResolvableType returnType = ResolvableType.forMethodReturnType(method, SomeRepository.class); + + ResolvableType arg0 = ResolvableType.forMethodParameter(method, 0, SomeRepository.class); // generic[0]=T + ResolvableType arg1 = ResolvableType.forMethodParameter(method, 1, SomeRepository.class); // generic[0]=? + ResolvableType arg2 = ResolvableType.forMethodParameter(method, 2, SomeRepository.class); // generic[0]=java.lang.Object + + assertThat(returnType.equalsType(arg0.as(Class.class).getGeneric(0))).isTrue(); + assertThat(returnType.equalsType(arg1.as(Class.class).getGeneric(0))).isFalse(); + assertThat(returnType.equalsType(arg2.as(Class.class).getGeneric(0))).isFalse(); + } + @Test void classType() throws Exception { ResolvableType type = ResolvableType.forField(Fields.class.getField("classType")); @@ -343,7 +360,7 @@ void getComponentTypeForClassArray() throws Exception { ResolvableType type = ResolvableType.forField(field); assertThat(type.isArray()).isTrue(); assertThat(type.getComponentType().getType()) - .isEqualTo(((Class) field.getGenericType()).getComponentType()); + .isEqualTo(((Class) field.getGenericType()).componentType()); } @Test @@ -355,7 +372,7 @@ void getComponentTypeForGenericArrayType() throws Exception { } @Test - void getComponentTypeForVariableThatResolvesToGenericArray() throws Exception { + void getComponentTypeForVariableThatResolvesToGenericArray() { ResolvableType type = ResolvableType.forClass(ListOfGenericArray.class).asCollection().getGeneric(); assertThat(type.isArray()).isTrue(); assertThat(type.getType()).isInstanceOf(TypeVariable.class); @@ -364,21 +381,21 @@ void getComponentTypeForVariableThatResolvesToGenericArray() throws Exception { } @Test - void getComponentTypeForNonArray() throws Exception { + void getComponentTypeForNonArray() { ResolvableType type = ResolvableType.forClass(String.class); assertThat(type.isArray()).isFalse(); assertThat(type.getComponentType()).isEqualTo(ResolvableType.NONE); } @Test - void asCollection() throws Exception { + void asCollection() { ResolvableType type = ResolvableType.forClass(ExtendsList.class).asCollection(); assertThat(type.resolve()).isEqualTo(Collection.class); assertThat(type.resolveGeneric()).isEqualTo(CharSequence.class); } @Test - void asMap() throws Exception { + void asMap() { ResolvableType type = ResolvableType.forClass(ExtendsMap.class).asMap(); assertThat(type.resolve()).isEqualTo(Map.class); assertThat(type.resolveGeneric(0)).isEqualTo(String.class); @@ -386,43 +403,43 @@ void asMap() throws Exception { } @Test - void asFromInterface() throws Exception { + void asFromInterface() { ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(List.class); assertThat(type.getType().toString()).isEqualTo("java.util.List"); } @Test - void asFromInheritedInterface() throws Exception { + void asFromInheritedInterface() { ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(Collection.class); assertThat(type.getType().toString()).isEqualTo("java.util.Collection"); } @Test - void asFromSuperType() throws Exception { + void asFromSuperType() { ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(ArrayList.class); assertThat(type.getType().toString()).isEqualTo("java.util.ArrayList"); } @Test - void asFromInheritedSuperType() throws Exception { + void asFromInheritedSuperType() { ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(List.class); assertThat(type.getType().toString()).isEqualTo("java.util.List"); } @Test - void asNotFound() throws Exception { + void asNotFound() { ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(Map.class); assertThat(type).isSameAs(ResolvableType.NONE); } @Test - void asSelf() throws Exception { + void asSelf() { ResolvableType type = ResolvableType.forClass(ExtendsList.class); assertThat(type.as(ExtendsList.class)).isEqualTo(type); } @Test - void getSuperType() throws Exception { + void getSuperType() { ResolvableType type = ResolvableType.forClass(ExtendsList.class).getSuperType(); assertThat(type.resolve()).isEqualTo(ArrayList.class); type = type.getSuperType(); @@ -434,7 +451,7 @@ void getSuperType() throws Exception { } @Test - void getInterfaces() throws Exception { + void getInterfaces() { ResolvableType type = ResolvableType.forClass(ExtendsList.class); assertThat(type.getInterfaces()).isEmpty(); SortedSet interfaces = new TreeSet<>(); @@ -447,13 +464,13 @@ void getInterfaces() throws Exception { } @Test - void noSuperType() throws Exception { + void noSuperType() { assertThat(ResolvableType.forClass(Object.class).getSuperType()) .isEqualTo(ResolvableType.NONE); } @Test - void noInterfaces() throws Exception { + void noInterfaces() { assertThat(ResolvableType.forClass(Object.class).getInterfaces()).isEmpty(); } @@ -517,7 +534,7 @@ void getGenericOfGenericByIndexes() throws Exception { } @Test - void getGenericOutOfBounds() throws Exception { + void getGenericOutOfBounds() { ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); assertThat(type.getGeneric(0)).isNotEqualTo(ResolvableType.NONE); assertThat(type.getGeneric(1)).isEqualTo(ResolvableType.NONE); @@ -525,14 +542,14 @@ void getGenericOutOfBounds() throws Exception { } @Test - void hasGenerics() throws Exception { + void hasGenerics() { ResolvableType type = ResolvableType.forClass(ExtendsList.class); assertThat(type.hasGenerics()).isFalse(); assertThat(type.asCollection().hasGenerics()).isTrue(); } @Test - void getGenericsFromParameterizedType() throws Exception { + void getGenericsFromParameterizedType() { ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); ResolvableType[] generics = type.getGenerics(); assertThat(generics).hasSize(1); @@ -540,7 +557,7 @@ void getGenericsFromParameterizedType() throws Exception { } @Test - void getGenericsFromClass() throws Exception { + void getGenericsFromClass() { ResolvableType type = ResolvableType.forClass(List.class); ResolvableType[] generics = type.getGenerics(); assertThat(generics).hasSize(1); @@ -548,14 +565,14 @@ void getGenericsFromClass() throws Exception { } @Test - void noGetGenerics() throws Exception { + void noGetGenerics() { ResolvableType type = ResolvableType.forClass(ExtendsList.class); ResolvableType[] generics = type.getGenerics(); assertThat(generics).isEmpty(); } @Test - void getResolvedGenerics() throws Exception { + void getResolvedGenerics() { ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); Class[] generics = type.resolveGenerics(); assertThat(generics).hasSize(1); @@ -680,6 +697,14 @@ void doesResolveFromOuterOwner() throws Exception { assertThat(type.getGeneric(0).as(Collection.class).getGeneric(0).as(Collection.class).resolve()).isNull(); } + @Test + void intArrayNotAssignableToIntegerArray() throws Exception { + ResolvableType integerArray = ResolvableType.forField(Fields.class.getField("integerArray")); + ResolvableType intArray = ResolvableType.forField(Fields.class.getField("intArray")); + assertThat(integerArray.isAssignableFrom(intArray)).isFalse(); + assertThat(intArray.isAssignableFrom(integerArray)).isFalse(); + } + @Test void resolveBoundedTypeVariableResult() throws Exception { ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableResult")); @@ -743,14 +768,14 @@ void resolveTypeVariableFromFieldTypeWithImplementsType() throws Exception { } @Test - void resolveTypeVariableFromSuperType() throws Exception { + void resolveTypeVariableFromSuperType() { ResolvableType type = ResolvableType.forClass(ExtendsList.class); assertThat(type.resolve()).isEqualTo(ExtendsList.class); assertThat(type.asCollection().resolveGeneric()).isEqualTo(CharSequence.class); } @Test - void resolveTypeVariableFromClassWithImplementsClass() throws Exception { + void resolveTypeVariableFromClassWithImplementsClass() { ResolvableType type = ResolvableType.forClass( MySuperclassType.class, MyCollectionSuperclassType.class); assertThat(type.resolveGeneric()).isEqualTo(Collection.class); @@ -946,7 +971,7 @@ void resolveFromOuterClass() throws Exception { } @Test - void resolveFromClassWithGenerics() throws Exception { + void resolveFromClassWithGenerics() { ResolvableType type = ResolvableType.forClassWithGenerics(List.class, ResolvableType.forClassWithGenerics(List.class, String.class)); assertThat(type.asCollection().toString()).isEqualTo("java.util.Collection>"); assertThat(type.asCollection().getGeneric().toString()).isEqualTo("java.util.List"); @@ -956,21 +981,21 @@ void resolveFromClassWithGenerics() throws Exception { } @Test - void isAssignableFromMustNotBeNull() throws Exception { + void isAssignableFromMustNotBeNull() { assertThatIllegalArgumentException() .isThrownBy(() -> ResolvableType.forClass(Object.class).isAssignableFrom((ResolvableType) null)) .withMessage("ResolvableType must not be null"); } @Test - void isAssignableFromForNone() throws Exception { + void isAssignableFromForNone() { ResolvableType objectType = ResolvableType.forClass(Object.class); assertThat(objectType.isAssignableFrom(ResolvableType.NONE)).isFalse(); assertThat(ResolvableType.NONE.isAssignableFrom(objectType)).isFalse(); } @Test - void isAssignableFromForClassAndClass() throws Exception { + void isAssignableFromForClassAndClass() { ResolvableType objectType = ResolvableType.forClass(Object.class); ResolvableType charSequenceType = ResolvableType.forClass(CharSequence.class); ResolvableType stringType = ResolvableType.forClass(String.class); @@ -1198,7 +1223,7 @@ void javaDocSample() throws Exception { } @Test - void forClassWithGenerics() throws Exception { + void forClassWithGenerics() { ResolvableType elementType = ResolvableType.forClassWithGenerics(Map.class, Integer.class, String.class); ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); assertThat(listType.toString()).isEqualTo("java.util.List>"); @@ -1207,13 +1232,13 @@ void forClassWithGenerics() throws Exception { } @Test - void classWithGenericsAs() throws Exception { + void classWithGenericsAs() { ResolvableType type = ResolvableType.forClassWithGenerics(MultiValueMap.class, Integer.class, String.class); assertThat(type.asMap().toString()).isEqualTo("java.util.Map>"); } @Test - void forClassWithMismatchedGenerics() throws Exception { + void forClassWithMismatchedGenerics() { assertThatIllegalArgumentException() .isThrownBy(() -> ResolvableType.forClassWithGenerics(Map.class, Integer.class)) .withMessageContaining("Mismatched number of generics specified for") @@ -1241,7 +1266,7 @@ void serialize() throws Exception { } @Test - void canResolveVoid() throws Exception { + void canResolveVoid() { ResolvableType type = ResolvableType.forClass(void.class); assertThat(type.resolve()).isEqualTo(void.class); } @@ -1260,19 +1285,19 @@ void hasUnresolvableGenerics() throws Exception { } @Test - void hasUnresolvableGenericsBasedOnOwnGenerics() throws Exception { + void hasUnresolvableGenericsBasedOnOwnGenerics() { ResolvableType type = ResolvableType.forClass(List.class); assertThat(type.hasUnresolvableGenerics()).isTrue(); } @Test - void hasUnresolvableGenericsWhenSelfNotResolvable() throws Exception { + void hasUnresolvableGenericsWhenSelfNotResolvable() { ResolvableType type = ResolvableType.forClass(List.class).getGeneric(); assertThat(type.hasUnresolvableGenerics()).isFalse(); } @Test - void hasUnresolvableGenericsWhenImplementingRawInterface() throws Exception { + void hasUnresolvableGenericsWhenImplementingRawInterface() { ResolvableType type = ResolvableType.forClass(MySimpleInterfaceTypeWithImplementsRaw.class); for (ResolvableType generic : type.getGenerics()) { assertThat(generic.resolve()).isNotNull(); @@ -1281,7 +1306,7 @@ void hasUnresolvableGenericsWhenImplementingRawInterface() throws Exception { } @Test - void hasUnresolvableGenericsWhenExtends() throws Exception { + void hasUnresolvableGenericsWhenExtends() { ResolvableType type = ResolvableType.forClass(ExtendsMySimpleInterfaceTypeWithImplementsRaw.class); for (ResolvableType generic : type.getGenerics()) { assertThat(generic.resolve()).isNotNull(); @@ -1297,7 +1322,7 @@ void spr11219() throws Exception { } @Test - void spr12701() throws Exception { + void spr12701() { ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Callable.class, String.class); Type type = resolvableType.getType(); assertThat(type).isInstanceOf(ParameterizedType.class); @@ -1363,6 +1388,12 @@ static class ExtendsMap extends HashMap { } + interface SomeRepository { + + T someMethod(Class arg0, Class arg1, Class arg2); + } + + static class Fields { public List classType; @@ -1404,6 +1435,10 @@ static class Fields { public Map, Map> nested; public T[] variableTypeGenericArray; + + public Integer[] integerArray; + + public int[] intArray; } diff --git a/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java b/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java index 5d5dc9dd5724..a9fc3167cd25 100644 --- a/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java +++ b/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,22 @@ package org.springframework.core; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.util.StringValueResolver; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** - * Unit tests for {@link SimpleAliasRegistry}. + * Tests for {@link SimpleAliasRegistry}. * * @author Juergen Hoeller * @author Nha Vuong @@ -29,70 +39,344 @@ */ class SimpleAliasRegistryTests { + private static final String REAL_NAME = "real_name"; + private static final String NICKNAME = "nickname"; + private static final String NAME1 = "name1"; + private static final String NAME2 = "name2"; + private static final String NAME3 = "name3"; + private static final String NAME4 = "name4"; + private static final String NAME5 = "name5"; + private static final String ALIAS1 = "alias1"; + private static final String ALIAS2 = "alias2"; + private static final String ALIAS3 = "alias3"; + // TODO Determine if we can make SimpleAliasRegistry.resolveAliases() reliable. + // See https://github.com/spring-projects/spring-framework/issues/32024. + // When ALIAS4 is changed to "test", various tests fail due to the iteration + // order of the entries in the aliasMap in SimpleAliasRegistry. + private static final String ALIAS4 = "alias4"; + private static final String ALIAS5 = "alias5"; + + private final SimpleAliasRegistry registry = new SimpleAliasRegistry(); + @Test void aliasChaining() { - registry.registerAlias("test", "testAlias"); - registry.registerAlias("testAlias", "testAlias2"); - registry.registerAlias("testAlias2", "testAlias3"); + registerAlias(NAME1, ALIAS1); + registerAlias(ALIAS1, ALIAS2); + registerAlias(ALIAS2, ALIAS3); - assertThat(registry.hasAlias("test", "testAlias")).isTrue(); - assertThat(registry.hasAlias("test", "testAlias2")).isTrue(); - assertThat(registry.hasAlias("test", "testAlias3")).isTrue(); - assertThat(registry.canonicalName("testAlias")).isEqualTo("test"); - assertThat(registry.canonicalName("testAlias2")).isEqualTo("test"); - assertThat(registry.canonicalName("testAlias3")).isEqualTo("test"); + assertHasAlias(NAME1, ALIAS1); + assertHasAlias(NAME1, ALIAS2); + assertHasAlias(NAME1, ALIAS3); + assertThat(registry.canonicalName(ALIAS1)).isEqualTo(NAME1); + assertThat(registry.canonicalName(ALIAS2)).isEqualTo(NAME1); + assertThat(registry.canonicalName(ALIAS3)).isEqualTo(NAME1); } @Test // SPR-17191 void aliasChainingWithMultipleAliases() { - registry.registerAlias("name", "alias_a"); - registry.registerAlias("name", "alias_b"); - assertThat(registry.hasAlias("name", "alias_a")).isTrue(); - assertThat(registry.hasAlias("name", "alias_b")).isTrue(); + registerAlias(NAME1, ALIAS1); + registerAlias(NAME1, ALIAS2); + assertHasAlias(NAME1, ALIAS1); + assertHasAlias(NAME1, ALIAS2); - registry.registerAlias("real_name", "name"); - assertThat(registry.hasAlias("real_name", "name")).isTrue(); - assertThat(registry.hasAlias("real_name", "alias_a")).isTrue(); - assertThat(registry.hasAlias("real_name", "alias_b")).isTrue(); + registerAlias(REAL_NAME, NAME1); + assertHasAlias(REAL_NAME, NAME1); + assertHasAlias(REAL_NAME, ALIAS1); + assertHasAlias(REAL_NAME, ALIAS2); - registry.registerAlias("name", "alias_c"); - assertThat(registry.hasAlias("real_name", "name")).isTrue(); - assertThat(registry.hasAlias("real_name", "alias_a")).isTrue(); - assertThat(registry.hasAlias("real_name", "alias_b")).isTrue(); - assertThat(registry.hasAlias("real_name", "alias_c")).isTrue(); + registerAlias(NAME1, ALIAS3); + assertHasAlias(REAL_NAME, NAME1); + assertHasAlias(REAL_NAME, ALIAS1); + assertHasAlias(REAL_NAME, ALIAS2); + assertHasAlias(REAL_NAME, ALIAS3); } @Test void removeAlias() { - registry.registerAlias("real_name", "nickname"); - assertThat(registry.hasAlias("real_name", "nickname")).isTrue(); + registerAlias(REAL_NAME, NICKNAME); + assertHasAlias(REAL_NAME, NICKNAME); - registry.removeAlias("nickname"); - assertThat(registry.hasAlias("real_name", "nickname")).isFalse(); + registry.removeAlias(NICKNAME); + assertDoesNotHaveAlias(REAL_NAME, NICKNAME); } @Test void isAlias() { - registry.registerAlias("real_name", "nickname"); - assertThat(registry.isAlias("nickname")).isTrue(); - assertThat(registry.isAlias("real_name")).isFalse(); - assertThat(registry.isAlias("fake")).isFalse(); + registerAlias(REAL_NAME, NICKNAME); + assertThat(registry.isAlias(NICKNAME)).isTrue(); + assertThat(registry.isAlias(REAL_NAME)).isFalse(); + assertThat(registry.isAlias("bogus")).isFalse(); } @Test void getAliases() { - registry.registerAlias("test", "testAlias1"); - assertThat(registry.getAliases("test")).containsExactly("testAlias1"); + assertThat(registry.getAliases(NAME1)).isEmpty(); + + registerAlias(NAME1, ALIAS1); + assertThat(registry.getAliases(NAME1)).containsExactly(ALIAS1); + + registerAlias(ALIAS1, ALIAS2); + registerAlias(ALIAS2, ALIAS3); + assertThat(registry.getAliases(NAME1)).containsExactlyInAnyOrder(ALIAS1, ALIAS2, ALIAS3); + assertThat(registry.getAliases(ALIAS1)).containsExactlyInAnyOrder(ALIAS2, ALIAS3); + assertThat(registry.getAliases(ALIAS2)).containsExactly(ALIAS3); + assertThat(registry.getAliases(ALIAS3)).isEmpty(); + } + + @Test + void checkForAliasCircle() { + // No aliases registered, so no cycles possible. + assertThatNoException().isThrownBy(() -> registry.checkForAliasCircle(NAME1, ALIAS1)); + + registerAlias(NAME1, ALIAS1); // ALIAS1 -> NAME1 + + // No cycles possible. + assertThatNoException().isThrownBy(() -> registry.checkForAliasCircle(NAME1, ALIAS1)); + + assertThatIllegalStateException() + // NAME1 -> ALIAS1 -> NAME1 + .isThrownBy(() -> registerAlias(ALIAS1, NAME1)) // internally invokes checkForAliasCircle() + .withMessageContaining("'%s' is a direct or indirect alias for '%s'", ALIAS1, NAME1); + + registerAlias(ALIAS1, ALIAS2); // ALIAS2 -> ALIAS1 -> NAME1 + assertThatIllegalStateException() + // NAME1 -> ALIAS1 -> ALIAS2 -> NAME1 + .isThrownBy(() -> registerAlias(ALIAS2, NAME1)) // internally invokes checkForAliasCircle() + .withMessageContaining("'%s' is a direct or indirect alias for '%s'", ALIAS2, NAME1); + } + + @Test + void resolveAliasesPreconditions() { + assertThatIllegalArgumentException().isThrownBy(() -> registry.resolveAliases(null)); + } + + @Test + void resolveAliasesWithoutPlaceholderReplacement() { + StringValueResolver valueResolver = new StubStringValueResolver(); + + registerAlias(NAME1, ALIAS1); + registerAlias(NAME1, ALIAS3); + registerAlias(NAME2, ALIAS2); + registerAlias(NAME2, ALIAS4); + assertThat(registry.getAliases(NAME1)).containsExactlyInAnyOrder(ALIAS1, ALIAS3); + assertThat(registry.getAliases(NAME2)).containsExactlyInAnyOrder(ALIAS2, ALIAS4); + + registry.resolveAliases(valueResolver); + assertThat(registry.getAliases(NAME1)).containsExactlyInAnyOrder(ALIAS1, ALIAS3); + assertThat(registry.getAliases(NAME2)).containsExactlyInAnyOrder(ALIAS2, ALIAS4); + + registry.removeAlias(ALIAS1); + registry.resolveAliases(valueResolver); + assertThat(registry.getAliases(NAME1)).containsExactly(ALIAS3); + assertThat(registry.getAliases(NAME2)).containsExactlyInAnyOrder(ALIAS2, ALIAS4); + } + + @Test + void resolveAliasesWithPlaceholderReplacement() { + StringValueResolver valueResolver = new StubStringValueResolver(Map.of( + NAME1, NAME2, + ALIAS1, ALIAS2 + )); + + registerAlias(NAME1, ALIAS1); + assertThat(registry.getAliases(NAME1)).containsExactly(ALIAS1); + + registry.resolveAliases(valueResolver); + assertThat(registry.getAliases(NAME1)).isEmpty(); + assertThat(registry.getAliases(NAME2)).containsExactly(ALIAS2); + + registry.removeAlias(ALIAS2); + assertThat(registry.getAliases(NAME1)).isEmpty(); + assertThat(registry.getAliases(NAME2)).isEmpty(); + } + + @Test + void resolveAliasesWithPlaceholderReplacementConflict() { + StringValueResolver valueResolver = new StubStringValueResolver(Map.of(ALIAS1, ALIAS2)); + + registerAlias(NAME1, ALIAS1); + registerAlias(NAME2, ALIAS2); + + // Original state: + // ALIAS1 -> NAME1 + // ALIAS2 -> NAME2 + + // State after processing original entry (ALIAS1 -> NAME1): + // ALIAS2 -> NAME1 --> Conflict: entry for ALIAS2 already exists + // ALIAS2 -> NAME2 + + assertThatIllegalStateException() + .isThrownBy(() -> registry.resolveAliases(valueResolver)) + .withMessage("Cannot register resolved alias '%s' (original: '%s') for name '%s': " + + "It is already registered for name '%s'.", ALIAS2, ALIAS1, NAME1, NAME2); + } + + @Test + void resolveAliasesWithComplexPlaceholderReplacement() { + StringValueResolver valueResolver = new StubStringValueResolver(Map.of( + ALIAS3, ALIAS1, + ALIAS4, ALIAS5, + ALIAS5, ALIAS2 + )); + + registerAlias(NAME3, ALIAS3); + registerAlias(NAME4, ALIAS4); + registerAlias(NAME5, ALIAS5); + + // Original state: + // WARNING: Based on ConcurrentHashMap iteration order! + // ALIAS3 -> NAME3 + // ALIAS5 -> NAME5 + // ALIAS4 -> NAME4 + + // State after processing original entry (ALIAS3 -> NAME3): + // ALIAS1 -> NAME3 + // ALIAS5 -> NAME5 + // ALIAS4 -> NAME4 + + // State after processing original entry (ALIAS5 -> NAME5): + // ALIAS1 -> NAME3 + // ALIAS2 -> NAME5 + // ALIAS4 -> NAME4 + + // State after processing original entry (ALIAS4 -> NAME4): + // ALIAS1 -> NAME3 + // ALIAS2 -> NAME5 + // ALIAS5 -> NAME4 + + registry.resolveAliases(valueResolver); + assertThat(registry.getAliases(NAME3)).containsExactly(ALIAS1); + assertThat(registry.getAliases(NAME4)).containsExactly(ALIAS5); + assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2); + } + + // TODO Remove this test once we have implemented reliable processing in SimpleAliasRegistry.resolveAliases(). + // See https://github.com/spring-projects/spring-framework/issues/32024. + // This method effectively duplicates the @ParameterizedTest version below, + // with aliasX hard coded to ALIAS4; however, this method also hard codes + // a different outcome that passes based on ConcurrentHashMap iteration order! + @Test + void resolveAliasesWithComplexPlaceholderReplacementAndNameSwitching() { + StringValueResolver valueResolver = new StubStringValueResolver(Map.of( + NAME3, NAME4, + NAME4, NAME3, + ALIAS3, ALIAS1, + ALIAS4, ALIAS5, + ALIAS5, ALIAS2 + )); + + registerAlias(NAME3, ALIAS3); + registerAlias(NAME4, ALIAS4); + registerAlias(NAME5, ALIAS5); + + // Original state: + // WARNING: Based on ConcurrentHashMap iteration order! + // ALIAS3 -> NAME3 + // ALIAS5 -> NAME5 + // ALIAS4 -> NAME4 + + // State after processing original entry (ALIAS3 -> NAME3): + // ALIAS1 -> NAME4 + // ALIAS5 -> NAME5 + // ALIAS4 -> NAME4 + + // State after processing original entry (ALIAS5 -> NAME5): + // ALIAS1 -> NAME4 + // ALIAS2 -> NAME5 + // ALIAS4 -> NAME4 + + // State after processing original entry (ALIAS4 -> NAME4): + // ALIAS1 -> NAME4 + // ALIAS2 -> NAME5 + // ALIAS5 -> NAME3 + + registry.resolveAliases(valueResolver); + assertThat(registry.getAliases(NAME3)).containsExactly(ALIAS5); + assertThat(registry.getAliases(NAME4)).containsExactly(ALIAS1); + assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2); + } + + @Disabled("Fails for some values unless alias registration order is honored") + @ParameterizedTest // gh-32024 + @ValueSource(strings = {"alias4", "test", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}) + void resolveAliasesWithComplexPlaceholderReplacementAndNameSwitching(String aliasX) { + StringValueResolver valueResolver = new StubStringValueResolver(Map.of( + NAME3, NAME4, + NAME4, NAME3, + ALIAS3, ALIAS1, + aliasX, ALIAS5, + ALIAS5, ALIAS2 + )); + + // If SimpleAliasRegistry ensures that aliases are processed in declaration + // order, we need to register ALIAS5 *before* aliasX to support our use case. + registerAlias(NAME3, ALIAS3); + registerAlias(NAME5, ALIAS5); + registerAlias(NAME4, aliasX); + + // Original state: + // WARNING: Based on LinkedHashMap iteration order! + // ALIAS3 -> NAME3 + // ALIAS5 -> NAME5 + // aliasX -> NAME4 + + // State after processing original entry (ALIAS3 -> NAME3): + // ALIAS5 -> NAME5 + // aliasX -> NAME4 + // ALIAS1 -> NAME4 + + // State after processing original entry (ALIAS5 -> NAME5): + // aliasX -> NAME4 + // ALIAS1 -> NAME4 + // ALIAS2 -> NAME5 + + // State after processing original entry (aliasX -> NAME4): + // ALIAS1 -> NAME4 + // ALIAS2 -> NAME5 + // alias5 -> NAME3 + + registry.resolveAliases(valueResolver); + assertThat(registry.getAliases(NAME3)).containsExactly(ALIAS5); + assertThat(registry.getAliases(NAME4)).containsExactly(ALIAS1); + assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2); + } + + private void registerAlias(String name, String alias) { + registry.registerAlias(name, alias); + } + + private void assertHasAlias(String name, String alias) { + assertThat(registry.hasAlias(name, alias)).isTrue(); + } + + private void assertDoesNotHaveAlias(String name, String alias) { + assertThat(registry.hasAlias(name, alias)).isFalse(); + } + + + /** + * {@link StringValueResolver} that replaces each value with a supplied + * placeholder and otherwise returns the original value if no placeholder + * is configured. + */ + private static class StubStringValueResolver implements StringValueResolver { + + private final Map placeholders; + + StubStringValueResolver() { + this(Map.of()); + } - registry.registerAlias("testAlias1", "testAlias2"); - registry.registerAlias("testAlias2", "testAlias3"); - assertThat(registry.getAliases("test")).containsExactlyInAnyOrder("testAlias1", "testAlias2", "testAlias3"); - assertThat(registry.getAliases("testAlias1")).containsExactlyInAnyOrder("testAlias2", "testAlias3"); - assertThat(registry.getAliases("testAlias2")).containsExactly("testAlias3"); + StubStringValueResolver(Map placeholders) { + this.placeholders = placeholders; + } - assertThat(registry.getAliases("testAlias3")).isEmpty(); + @Override + public String resolveStringValue(String str) { + return this.placeholders.getOrDefault(str, str); + } } } diff --git a/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java b/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java index 491be2a659ef..e58e84096153 100644 --- a/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java +++ b/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.entry; /** - * Unit tests for {@link SortedProperties}. + * Tests for {@link SortedProperties}. * * @author Sam Brannen * @since 5.2 diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 3eba643295e1..bf6a82bb5751 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ import static org.springframework.core.annotation.AnnotationUtilsTests.asArray; /** - * Unit tests for {@link AnnotatedElementUtils}. + * Tests for {@link AnnotatedElementUtils}. * * @author Sam Brannen * @author Rossen Stoyanchev @@ -171,8 +171,8 @@ void findMergedAnnotationWithSingleElementOverridingAnArrayViaConvention() throw @Test void getMetaAnnotationTypesOnNonAnnotatedClass() { - assertThat(getMetaAnnotationTypes(NonAnnotatedClass.class, TransactionalComponent.class).isEmpty()).isTrue(); - assertThat(getMetaAnnotationTypes(NonAnnotatedClass.class, TransactionalComponent.class.getName()).isEmpty()).isTrue(); + assertThat(getMetaAnnotationTypes(NonAnnotatedClass.class, TransactionalComponent.class)).isEmpty(); + assertThat(getMetaAnnotationTypes(NonAnnotatedClass.class, TransactionalComponent.class.getName())).isEmpty(); } @Test @@ -293,7 +293,7 @@ void getAllAnnotationAttributesOnNonAnnotatedClass() { void getAllAnnotationAttributesOnClassWithLocalAnnotation() { MultiValueMap attributes = getAllAnnotationAttributes(TxConfig.class, TX_NAME); assertThat(attributes).as("Annotation attributes map for @Transactional on TxConfig").isNotNull(); - assertThat(attributes.get("value")).as("value for TxConfig").isEqualTo(asList("TxConfig")); + assertThat(attributes.get("value")).as("value for TxConfig").isEqualTo(List.of("TxConfig")); } @Test @@ -307,14 +307,14 @@ void getAllAnnotationAttributesOnClassWithLocalComposedAnnotationAndInheritedAnn void getAllAnnotationAttributesFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap attributes = getAllAnnotationAttributes(SubSubClassWithInheritedAnnotation.class, TX_NAME); assertThat(attributes).as("Annotation attributes map for @Transactional on SubSubClassWithInheritedAnnotation").isNotNull(); - assertThat(attributes.get("qualifier")).isEqualTo(asList("transactionManager")); + assertThat(attributes.get("qualifier")).isEqualTo(List.of("transactionManager")); } @Test void getAllAnnotationAttributesFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap attributes = getAllAnnotationAttributes( SubSubClassWithInheritedComposedAnnotation.class, TX_NAME); assertThat(attributes).as("Annotation attributes map for @Transactional on SubSubClassWithInheritedComposedAnnotation").isNotNull(); - assertThat(attributes.get("qualifier")).isEqualTo(asList("composed1")); + assertThat(attributes.get("qualifier")).isEqualTo(List.of("composed1")); } /** @@ -329,7 +329,7 @@ void getAllAnnotationAttributesOnClassWithLocalAnnotationThatShadowsAnnotationFr // See org.springframework.core.env.EnvironmentSystemIntegrationTests#mostSpecificDerivedClassDrivesEnvironment_withDevEnvAndDerivedDevConfigClass MultiValueMap attributes = getAllAnnotationAttributes(DerivedTxConfig.class, TX_NAME); assertThat(attributes).as("Annotation attributes map for @Transactional on DerivedTxConfig").isNotNull(); - assertThat(attributes.get("value")).as("value for DerivedTxConfig").isEqualTo(asList("DerivedTxConfig")); + assertThat(attributes.get("value")).as("value for DerivedTxConfig").isEqualTo(List.of("DerivedTxConfig")); } /** @@ -348,7 +348,7 @@ void getAllAnnotationAttributesOnLangType() { MultiValueMap attributes = getAllAnnotationAttributes( NonNullApi.class, Nonnull.class.getName()); assertThat(attributes).as("Annotation attributes map for @Nonnull on NonNullApi").isNotNull(); - assertThat(attributes.get("when")).as("value for NonNullApi").isEqualTo(asList(When.ALWAYS)); + assertThat(attributes.get("when")).as("value for NonNullApi").isEqualTo(List.of(When.ALWAYS)); } @Test @@ -356,7 +356,7 @@ void getAllAnnotationAttributesOnJavaxType() { MultiValueMap attributes = getAllAnnotationAttributes( ParametersAreNonnullByDefault.class, Nonnull.class.getName()); assertThat(attributes).as("Annotation attributes map for @Nonnull on NonNullApi").isNotNull(); - assertThat(attributes.get("when")).as("value for NonNullApi").isEqualTo(asList(When.ALWAYS)); + assertThat(attributes.get("when")).as("value for NonNullApi").isEqualTo(List.of(When.ALWAYS)); } @Test @@ -752,7 +752,7 @@ void findMergedAnnotationOnMethodWithComposedMetaTransactionalAnnotation() throw * @see #23767 */ @Test - void findMergedAnnotationAttributesOnClassWithComposedMetaTransactionalAnnotation() throws Exception { + void findMergedAnnotationAttributesOnClassWithComposedMetaTransactionalAnnotation() { Class clazz = ComposedTransactionalClass.class; AnnotationAttributes attributes = findMergedAnnotationAttributes(clazz, AliasedTransactional.class); @@ -766,7 +766,7 @@ void findMergedAnnotationAttributesOnClassWithComposedMetaTransactionalAnnotatio * @see #23767 */ @Test - void findMergedAnnotationOnClassWithComposedMetaTransactionalAnnotation() throws Exception { + void findMergedAnnotationOnClassWithComposedMetaTransactionalAnnotation() { Class clazz = ComposedTransactionalClass.class; AliasedTransactional annotation = findMergedAnnotation(clazz, AliasedTransactional.class); @@ -848,13 +848,13 @@ void javaLangAnnotationTypeViaFindMergedAnnotation() throws Exception { } @Test - void javaxAnnotationTypeViaFindMergedAnnotation() throws Exception { + void javaxAnnotationTypeViaFindMergedAnnotation() { assertThat(findMergedAnnotation(ResourceHolder.class, Resource.class)).isEqualTo(ResourceHolder.class.getAnnotation(Resource.class)); assertThat(findMergedAnnotation(SpringAppConfigClass.class, Resource.class)).isEqualTo(SpringAppConfigClass.class.getAnnotation(Resource.class)); } @Test - void javaxMetaAnnotationTypeViaFindMergedAnnotation() throws Exception { + void javaxMetaAnnotationTypeViaFindMergedAnnotation() { assertThat(findMergedAnnotation(ParametersAreNonnullByDefault.class, Nonnull.class)).isEqualTo(ParametersAreNonnullByDefault.class.getAnnotation(Nonnull.class)); assertThat(findMergedAnnotation(ResourceHolder.class, Nonnull.class)).isEqualTo(ParametersAreNonnullByDefault.class.getAnnotation(Nonnull.class)); } @@ -869,7 +869,7 @@ void nullableAnnotationTypeViaFindMergedAnnotation() throws Exception { void getAllMergedAnnotationsOnClassWithInterface() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); Set allMergedAnnotations = getAllMergedAnnotations(method, Transactional.class); - assertThat(allMergedAnnotations.isEmpty()).isTrue(); + assertThat(allMergedAnnotations).isEmpty(); } @Test @@ -1388,7 +1388,7 @@ interface InterfaceWithInheritedAnnotation { void handleFromInterface(); } - static abstract class AbstractClassWithInheritedAnnotation implements InterfaceWithInheritedAnnotation { + abstract static class AbstractClassWithInheritedAnnotation implements InterfaceWithInheritedAnnotation { @Transactional public abstract void handle(); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java index 49ea67b0b296..953c2d370499 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link AnnotationAttributes}. + * Tests for {@link AnnotationAttributes}. * * @author Chris Beams * @author Sam Brannen @@ -72,7 +72,7 @@ void typeSafeAttributeAccess() { } @Test - void unresolvableClassWithClassNotFoundException() throws Exception { + void unresolvableClassWithClassNotFoundException() { attributes.put("unresolvableClass", new ClassNotFoundException("myclass")); assertThatIllegalArgumentException() .isThrownBy(() -> attributes.getClass("unresolvableClass")) @@ -81,7 +81,7 @@ void unresolvableClassWithClassNotFoundException() throws Exception { } @Test - void unresolvableClassWithLinkageError() throws Exception { + void unresolvableClassWithLinkageError() { attributes.put("unresolvableClass", new LinkageError("myclass")); assertThatIllegalArgumentException() .isThrownBy(() -> attributes.getClass("unresolvableClass")) @@ -90,7 +90,7 @@ void unresolvableClassWithLinkageError() throws Exception { } @Test - void singleElementToSingleElementArrayConversionSupport() throws Exception { + void singleElementToSingleElementArrayConversionSupport() { Filter filter = FilteredClass.class.getAnnotation(Filter.class); AnnotationAttributes nestedAttributes = new AnnotationAttributes(); @@ -118,7 +118,7 @@ void singleElementToSingleElementArrayConversionSupport() throws Exception { } @Test - void nestedAnnotations() throws Exception { + void nestedAnnotations() { Filter filter = FilteredClass.class.getAnnotation(Filter.class); attributes.put("filter", filter); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAwareOrderComparatorTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAwareOrderComparatorTests.java index 481163c306f5..967f4f9fc3c4 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAwareOrderComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAwareOrderComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,8 +41,7 @@ void sortInstances() { list.add(new B()); list.add(new A()); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isInstanceOf(A.class); - assertThat(list.get(1)).isInstanceOf(B.class); + assertThat(list).hasExactlyElementsOfTypes(A.class, B.class); } @Test @@ -51,8 +50,7 @@ void sortInstancesWithPriority() { list.add(new B2()); list.add(new A2()); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isInstanceOf(A2.class); - assertThat(list.get(1)).isInstanceOf(B2.class); + assertThat(list).hasExactlyElementsOfTypes(A2.class, B2.class); } @Test @@ -61,8 +59,7 @@ void sortInstancesWithOrderAndPriority() { list.add(new B()); list.add(new A2()); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isInstanceOf(A2.class); - assertThat(list.get(1)).isInstanceOf(B.class); + assertThat(list).hasExactlyElementsOfTypes(A2.class, B.class); } @Test @@ -71,8 +68,7 @@ void sortInstancesWithSubclass() { list.add(new B()); list.add(new C()); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isInstanceOf(C.class); - assertThat(list.get(1)).isInstanceOf(B.class); + assertThat(list).hasExactlyElementsOfTypes(C.class, B.class); } @Test @@ -81,8 +77,7 @@ void sortClasses() { list.add(B.class); list.add(A.class); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isEqualTo(A.class); - assertThat(list.get(1)).isEqualTo(B.class); + assertThat(list).containsExactly(A.class, B.class); } @Test @@ -91,8 +86,7 @@ void sortClassesWithSubclass() { list.add(B.class); list.add(C.class); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isEqualTo(C.class); - assertThat(list.get(1)).isEqualTo(B.class); + assertThat(list).containsExactly(C.class, B.class); } @Test @@ -103,13 +97,9 @@ void sortWithNulls() { list.add(null); list.add(A.class); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isEqualTo(A.class); - assertThat(list.get(1)).isEqualTo(B.class); - assertThat(list.get(2)).isNull(); - assertThat(list.get(3)).isNull(); + assertThat(list).containsExactly(A.class, B.class, null, null); } - @Order(1) private static class A { } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java index 0fa93c99e938..9750cd5ac2e8 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,55 +43,45 @@ class AnnotationIntrospectionFailureTests { @Test void filteredTypeThrowsTypeNotPresentException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleAnnotation = ClassUtils.forName( - WithExampleAnnotation.class.getName(), classLoader); - Annotation annotation = withExampleAnnotation.getAnnotations()[0]; + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleAnnotation.class.getName(), classLoader); + Annotation annotation = withAnnotation.getAnnotations()[0]; Method method = annotation.annotationType().getMethod("value"); method.setAccessible(true); - assertThatExceptionOfType(TypeNotPresentException.class).isThrownBy(() -> - ReflectionUtils.invokeMethod(method, annotation)) - .withCauseInstanceOf(ClassNotFoundException.class); + assertThatExceptionOfType(TypeNotPresentException.class) + .isThrownBy(() -> ReflectionUtils.invokeMethod(method, annotation)) + .withCauseInstanceOf(ClassNotFoundException.class); } @Test @SuppressWarnings("unchecked") void filteredTypeInMetaAnnotationWhenUsingAnnotatedElementUtilsHandlesException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleMetaAnnotation = ClassUtils.forName( - WithExampleMetaAnnotation.class.getName(), classLoader); - Class exampleAnnotationClass = (Class) ClassUtils.forName( - ExampleAnnotation.class.getName(), classLoader); - Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( - ExampleMetaAnnotation.class.getName(), classLoader); - assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( - withExampleMetaAnnotation, exampleAnnotationClass)).isNull(); - assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( - withExampleMetaAnnotation, exampleMetaAnnotationClass)).isNull(); - assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, - exampleAnnotationClass)).isFalse(); - assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, - exampleMetaAnnotationClass)).isFalse(); + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleMetaAnnotation.class.getName(), classLoader); + Class annotationClass = (Class) + ClassUtils.forName(ExampleAnnotation.class.getName(), classLoader); + Class metaAnnotationClass = (Class) + ClassUtils.forName(ExampleMetaAnnotation.class.getName(), classLoader); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes(withAnnotation, annotationClass)).isNull(); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes(withAnnotation, metaAnnotationClass)).isNull(); + assertThat(AnnotatedElementUtils.hasAnnotation(withAnnotation, annotationClass)).isFalse(); + assertThat(AnnotatedElementUtils.hasAnnotation(withAnnotation, metaAnnotationClass)).isFalse(); } @Test @SuppressWarnings("unchecked") void filteredTypeInMetaAnnotationWhenUsingMergedAnnotationsHandlesException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleMetaAnnotation = ClassUtils.forName( - WithExampleMetaAnnotation.class.getName(), classLoader); - Class exampleAnnotationClass = (Class) ClassUtils.forName( - ExampleAnnotation.class.getName(), classLoader); - Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( - ExampleMetaAnnotation.class.getName(), classLoader); - MergedAnnotations annotations = MergedAnnotations.from(withExampleMetaAnnotation); - assertThat(annotations.get(exampleAnnotationClass).isPresent()).isFalse(); - assertThat(annotations.get(exampleMetaAnnotationClass).isPresent()).isFalse(); - assertThat(annotations.isPresent(exampleMetaAnnotationClass)).isFalse(); - assertThat(annotations.isPresent(exampleAnnotationClass)).isFalse(); + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleMetaAnnotation.class.getName(), classLoader); + Class annotationClass = (Class) + ClassUtils.forName(ExampleAnnotation.class.getName(), classLoader); + Class metaAnnotationClass = (Class) + ClassUtils.forName(ExampleMetaAnnotation.class.getName(), classLoader); + MergedAnnotations annotations = MergedAnnotations.from(withAnnotation); + assertThat(annotations.get(annotationClass).isPresent()).isFalse(); + assertThat(annotations.get(metaAnnotationClass).isPresent()).isFalse(); + assertThat(annotations.isPresent(metaAnnotationClass)).isFalse(); + assertThat(annotations.isPresent(annotationClass)).isFalse(); } @@ -103,17 +93,16 @@ static class FilteringClassLoader extends OverridingClassLoader { @Override protected boolean isEligibleForOverriding(String className) { - return className.startsWith( - AnnotationIntrospectionFailureTests.class.getName()); + return className.startsWith(AnnotationIntrospectionFailureTests.class.getName()) || + className.startsWith("jdk.internal"); } @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith(AnnotationIntrospectionFailureTests.class.getName()) && - name.contains("Filtered")) { + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + if (name.contains("Filtered") || name.startsWith("jdk.internal")) { throw new ClassNotFoundException(name); } - return super.loadClass(name, resolve); + return super.loadClassForOverriding(name); } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index f181a1af13a8..d9843791665b 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -64,7 +63,7 @@ import static org.springframework.core.annotation.AnnotationUtils.synthesizeAnnotation; /** - * Unit tests for {@link AnnotationUtils}. + * Tests for {@link AnnotationUtils}. * * @author Rod Johnson * @author Juergen Hoeller @@ -160,9 +159,8 @@ void findMethodAnnotationOnBridgeMethod() throws Exception { assertThat(getAnnotation(bridgeMethod, Order.class)).isNull(); assertThat(findAnnotation(bridgeMethod, Order.class)).isNotNull(); - boolean runningInEclipse = Arrays.stream(new Exception().getStackTrace()) - .anyMatch(element -> element.getClassName().startsWith("org.eclipse.jdt")); - + boolean runningInEclipse = StackWalker.getInstance().walk(stream -> + stream.anyMatch(stackFrame -> stackFrame.getClassName().startsWith("org.eclipse.jdt"))); // As of JDK 8, invoking getAnnotation() on a bridge method actually finds an // annotation on its 'bridged' method [1]; however, the Eclipse compiler will not // support this until Eclipse 4.9 [2]. Thus, we effectively ignore the following @@ -323,8 +321,8 @@ void findClassAnnotationOnSubSubNonInheritedAnnotationInterface() { @Test void findAnnotationDeclaringClassForAllScenarios() { // no class-level annotation - assertThat((Object) findAnnotationDeclaringClass(Transactional.class, NonAnnotatedInterface.class)).isNull(); - assertThat((Object) findAnnotationDeclaringClass(Transactional.class, NonAnnotatedClass.class)).isNull(); + assertThat(findAnnotationDeclaringClass(Transactional.class, NonAnnotatedInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClass(Transactional.class, NonAnnotatedClass.class)).isNull(); // inherited class-level annotation; note: @Transactional is inherited assertThat(findAnnotationDeclaringClass(Transactional.class, InheritedAnnotationInterface.class)).isEqualTo(InheritedAnnotationInterface.class); @@ -344,8 +342,8 @@ void findAnnotationDeclaringClassForAllScenarios() { void findAnnotationDeclaringClassForTypesWithSingleCandidateType() { // no class-level annotation List> transactionalCandidateList = Collections.singletonList(Transactional.class); - assertThat((Object) findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedInterface.class)).isNull(); - assertThat((Object) findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedClass.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedClass.class)).isNull(); // inherited class-level annotation; note: @Transactional is inherited assertThat(findAnnotationDeclaringClassForTypes(transactionalCandidateList, InheritedAnnotationInterface.class)).isEqualTo(InheritedAnnotationInterface.class); @@ -367,8 +365,8 @@ void findAnnotationDeclaringClassForTypesWithMultipleCandidateTypes() { List> candidates = asList(Transactional.class, Order.class); // no class-level annotation - assertThat((Object) findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedInterface.class)).isNull(); - assertThat((Object) findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedClass.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedClass.class)).isNull(); // inherited class-level annotation; note: @Transactional is inherited assertThat(findAnnotationDeclaringClassForTypes(candidates, InheritedAnnotationInterface.class)).isEqualTo(InheritedAnnotationInterface.class); @@ -505,7 +503,7 @@ void getValueFromAnnotation() throws Exception { } @Test - void getValueFromNonPublicAnnotation() throws Exception { + void getValueFromNonPublicAnnotation() { Annotation[] declaredAnnotations = NonPublicAnnotatedClass.class.getDeclaredAnnotations(); assertThat(declaredAnnotations).hasSize(1); Annotation annotation = declaredAnnotations[0]; @@ -720,7 +718,7 @@ void getDeclaredRepeatableAnnotationsDeclaredOnSuperclass() { } @Test - void synthesizeAnnotationWithImplicitAliasesWithMissingDefaultValues() throws Exception { + void synthesizeAnnotationWithImplicitAliasesWithMissingDefaultValues() { Class clazz = ImplicitAliasesWithMissingDefaultValuesContextConfigClass.class; Class annotationType = ImplicitAliasesWithMissingDefaultValuesContextConfig.class; @@ -736,7 +734,7 @@ void synthesizeAnnotationWithImplicitAliasesWithMissingDefaultValues() throws Ex } @Test - void synthesizeAnnotationWithImplicitAliasesWithDifferentDefaultValues() throws Exception { + void synthesizeAnnotationWithImplicitAliasesWithDifferentDefaultValues() { Class clazz = ImplicitAliasesWithDifferentDefaultValuesContextConfigClass.class; Class annotationType = ImplicitAliasesWithDifferentDefaultValuesContextConfig.class; @@ -751,7 +749,7 @@ void synthesizeAnnotationWithImplicitAliasesWithDifferentDefaultValues() throws } @Test - void synthesizeAnnotationWithImplicitAliasesWithDuplicateValues() throws Exception { + void synthesizeAnnotationWithImplicitAliasesWithDuplicateValues() { Class clazz = ImplicitAliasesWithDuplicateValuesContextConfigClass.class; Class annotationType = ImplicitAliasesWithDuplicateValuesContextConfig.class; @@ -769,7 +767,7 @@ void synthesizeAnnotationWithImplicitAliasesWithDuplicateValues() throws Excepti } @Test - void synthesizeAnnotationFromMapWithoutAttributeAliases() throws Exception { + void synthesizeAnnotationFromMapWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); @@ -784,7 +782,7 @@ void synthesizeAnnotationFromMapWithoutAttributeAliases() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeAnnotationFromMapWithNestedMap() throws Exception { + void synthesizeAnnotationFromMapWithNestedMap() { ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation(ComponentScanSingleFilter.class); assertThat(componentScan).isNotNull(); @@ -813,7 +811,7 @@ void synthesizeAnnotationFromMapWithNestedMap() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeAnnotationFromMapWithNestedArrayOfMaps() throws Exception { + void synthesizeAnnotationFromMapWithNestedArrayOfMaps() { ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class); assertThat(componentScan).isNotNull(); @@ -843,7 +841,7 @@ void synthesizeAnnotationFromMapWithNestedArrayOfMaps() throws Exception { } @Test - void synthesizeAnnotationFromDefaultsWithoutAttributeAliases() throws Exception { + void synthesizeAnnotationFromDefaultsWithoutAttributeAliases() { AnnotationWithDefaults annotationWithDefaults = synthesizeAnnotation(AnnotationWithDefaults.class); assertThat(annotationWithDefaults).isNotNull(); assertThat(annotationWithDefaults.text()).as("text: ").isEqualTo("enigma"); @@ -852,7 +850,7 @@ void synthesizeAnnotationFromDefaultsWithoutAttributeAliases() throws Exception } @Test - void synthesizeAnnotationFromDefaultsWithAttributeAliases() throws Exception { + void synthesizeAnnotationFromDefaultsWithAttributeAliases() { ContextConfig contextConfig = synthesizeAnnotation(ContextConfig.class); assertThat(contextConfig).isNotNull(); assertThat(contextConfig.value()).as("value: ").isEmpty(); @@ -860,7 +858,7 @@ void synthesizeAnnotationFromDefaultsWithAttributeAliases() throws Exception { } @Test - void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() throws Exception { + void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() { Map map = Collections.singletonMap("location", "test.xml"); ContextConfig contextConfig = synthesizeAnnotation(map, ContextConfig.class, null); assertThat(contextConfig).isNotNull(); @@ -869,7 +867,7 @@ void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() thro } @Test - void synthesizeAnnotationFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() throws Exception { + void synthesizeAnnotationFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() { Map map = Collections.singletonMap("value", "/foo"); Get get = synthesizeAnnotation(map, Get.class, null); assertThat(get).isNotNull(); @@ -884,7 +882,7 @@ void synthesizeAnnotationFromMapWithAttributeAliasesThatOverrideArraysWithSingle } @Test - void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception { + void synthesizeAnnotationFromMapWithImplicitAttributeAliases() { assertAnnotationSynthesisFromMapWithImplicitAliases("value"); assertAnnotationSynthesisFromMapWithImplicitAliases("location1"); assertAnnotationSynthesisFromMapWithImplicitAliases("location2"); @@ -893,7 +891,7 @@ void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception assertAnnotationSynthesisFromMapWithImplicitAliases("groovyScript"); } - private void assertAnnotationSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) throws Exception { + private void assertAnnotationSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) { Map map = Collections.singletonMap(attributeNameAndValue, attributeNameAndValue); ImplicitAliasesContextConfig config = synthesizeAnnotation(map, ImplicitAliasesContextConfig.class, null); assertThat(config).isNotNull(); @@ -906,12 +904,12 @@ private void assertAnnotationSynthesisFromMapWithImplicitAliases(String attribut } @Test - void synthesizeAnnotationFromMapWithMissingAttributeValue() throws Exception { + void synthesizeAnnotationFromMapWithMissingAttributeValue() { assertMissingTextAttribute(Collections.emptyMap()); } @Test - void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception { + void synthesizeAnnotationFromMapWithNullAttributeValue() { Map map = Collections.singletonMap("text", null); assertThat(map.containsKey("text")).isTrue(); assertMissingTextAttribute(map); @@ -924,7 +922,7 @@ private void assertMissingTextAttribute(Map attributes) { } @Test - void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() throws Exception { + void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() { Map map = Collections.singletonMap(VALUE, 42L); assertThatIllegalStateException().isThrownBy(() -> synthesizeAnnotation(map, Component.class, null).value()) @@ -933,7 +931,7 @@ void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() throws Exception } @Test - void synthesizeAnnotationFromAnnotationAttributesWithoutAttributeAliases() throws Exception { + void synthesizeAnnotationFromAnnotationAttributesWithoutAttributeAliases() { // 1) Get an annotation Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); @@ -954,7 +952,7 @@ void synthesizeAnnotationFromAnnotationAttributesWithoutAttributeAliases() throw } @Test // gh-22702 - void findAnnotationWithRepeatablesElements() throws Exception { + void findAnnotationWithRepeatablesElements() { assertThat(AnnotationUtils.findAnnotation(TestRepeatablesClass.class, TestRepeatable.class)).isNull(); assertThat(AnnotationUtils.findAnnotation(TestRepeatablesClass.class, @@ -962,7 +960,7 @@ void findAnnotationWithRepeatablesElements() throws Exception { } @Test // gh-23856 - void findAnnotationFindsRepeatableContainerOnComposedAnnotationMetaAnnotatedWithRepeatableAnnotations() throws Exception { + void findAnnotationFindsRepeatableContainerOnComposedAnnotationMetaAnnotatedWithRepeatableAnnotations() { MyRepeatableContainer annotation = AnnotationUtils.findAnnotation(MyRepeatableMeta1And2.class, MyRepeatableContainer.class); assertThat(annotation).isNotNull(); @@ -979,7 +977,7 @@ void findAnnotationFindsRepeatableContainerOnComposedAnnotationMetaAnnotatedWith } @Test // gh-23929 - void findDeprecatedAnnotation() throws Exception { + void findDeprecatedAnnotation() { assertThat(getAnnotation(DeprecatedClass.class, Deprecated.class)).isNotNull(); assertThat(getAnnotation(SubclassOfDeprecatedClass.class, Deprecated.class)).isNull(); assertThat(findAnnotation(DeprecatedClass.class, Deprecated.class)).isNotNull(); @@ -1135,7 +1133,7 @@ public void overrideWithoutNewAnnotation() { boolean readOnly() default false; } - public static abstract class Foo { + public abstract static class Foo { @Order(1) public abstract void something(T arg); @@ -1245,7 +1243,7 @@ public void foo(String t) { } } - public static abstract class BaseClassWithGenericAnnotatedMethod { + public abstract static class BaseClassWithGenericAnnotatedMethod { @Order abstract void foo(T t); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java index 792dc1f6def8..6400a880c33c 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -197,7 +197,7 @@ void typeHierarchyStrategyOnClassWhenHasSuperclassScansSuperclass() { } @Test - void typeHierarchyStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + void typeHierarchyStrategyOnClassWhenHasSingleInterfaceScansInterfaces() { Class source = WithSingleInterface.class; assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); @@ -353,10 +353,19 @@ void typeHierarchyStrategyOnMethodWhenHasSuperclassScansSuperclass() { } @Test - void typeHierarchyStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + void typeHierarchyStrategyOnMethodWhenHasInterfaceScansInterfaces() { Method source = methodFrom(WithSingleInterface.class); assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + + source = methodFrom(Hello1Impl.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("1:TestAnnotation1"); + } + + @Test // gh-31803 + void typeHierarchyStrategyOnMethodWhenHasInterfaceHierarchyScansInterfacesOnlyOnce() { + Method source = methodFrom(Hello2Impl.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("1:TestAnnotation1"); } @Test @@ -691,6 +700,30 @@ public void method() { } } + interface Hello1 { + + @TestAnnotation1 + void method(); + } + + interface Hello2 extends Hello1 { + } + + static class Hello1Impl implements Hello1 { + + @Override + public void method() { + } + } + + static class Hello2Impl implements Hello2 { + + @Override + public void method() { + } + } + + @TestAnnotation2 @TestInheritedAnnotation2 static class HierarchySuperclass extends HierarchySuperSuperclass { @@ -770,7 +803,7 @@ interface IgnorableOverrideInterface2 { void method(); } - static abstract class MultipleMethods implements MultipleMethodsInterface { + abstract static class MultipleMethods implements MultipleMethodsInterface { @TestAnnotation1 public void method() { @@ -800,7 +833,7 @@ interface GenericOverrideInterface { void method(T argument); } - static abstract class GenericNonOverride implements GenericNonOverrideInterface { + abstract static class GenericNonOverride implements GenericNonOverrideInterface { @TestAnnotation1 public void method(StringBuilder argument) { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java index 8a4acb7b7978..a2b885601f72 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -112,7 +112,7 @@ void isValidWhenHasTypeNotPresentExceptionReturnsFalse() { ClassValue annotation = mockAnnotation(ClassValue.class); given(annotation.value()).willThrow(TypeNotPresentException.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.isValid(annotation)).isFalse(); + assertThat(attributes.canLoad(annotation)).isFalse(); } @Test @@ -121,7 +121,7 @@ void isValidWhenDoesNotHaveTypeNotPresentExceptionReturnsTrue() { ClassValue annotation = mock(); given(annotation.value()).willReturn((Class) InputStream.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.isValid(annotation)).isTrue(); + assertThat(attributes.canLoad(annotation)).isTrue(); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java index 154c9d9f4fa7..864a3e6302fc 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -161,8 +161,7 @@ void typeHierarchyStrategyComposedPlusLocalAnnotationsOnMethod() } @Test - void typeHierarchyStrategyMultipleComposedAnnotationsOnBridgeMethod() - throws Exception { + void typeHierarchyStrategyMultipleComposedAnnotationsOnBridgeMethod() { assertTypeHierarchyStrategyBehavior(getBridgeMethod()); } @@ -173,7 +172,7 @@ private void assertTypeHierarchyStrategyBehavior(AnnotatedElement element) { assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache"); } - Method getBridgeMethod() throws NoSuchMethodException { + Method getBridgeMethod() { List methods = new ArrayList<>(); ReflectionUtils.doWithLocalMethods(StringGenericParameter.class, method -> { if ("getFor".equals(method.getName())) { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java index 40b1fdf5b8f8..404073b9b42b 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; import java.util.Set; import java.util.stream.Stream; @@ -35,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.INHERITED_ANNOTATIONS; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY; /** * Tests for {@link MergedAnnotations} and {@link RepeatableContainers} that @@ -49,184 +52,168 @@ class MergedAnnotationsRepeatableAnnotationTests { @Test void inheritedAnnotationsWhenNonRepeatableThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> - getAnnotations(null, NonRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, getClass())) - .satisfies(this::nonRepeatableRequirements); + assertThatIllegalArgumentException() + .isThrownBy(() -> getAnnotations(null, NonRepeatable.class, INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::nonRepeatableRequirements); } @Test void inheritedAnnotationsWhenContainerMissingValueAttributeThrowsException() { - assertThatAnnotationConfigurationException().isThrownBy(() -> - getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class, - SearchStrategy.INHERITED_ANNOTATIONS, getClass())) - .satisfies(this::missingValueAttributeRequirements); + assertThatAnnotationConfigurationException() + .isThrownBy(() -> getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class, + INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::missingValueAttributeRequirements); } @Test void inheritedAnnotationsWhenWhenNonArrayValueAttributeThrowsException() { - assertThatAnnotationConfigurationException().isThrownBy(() -> - getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class, - SearchStrategy.INHERITED_ANNOTATIONS, getClass())) - .satisfies(this::nonArrayValueAttributeRequirements); + assertThatAnnotationConfigurationException() + .isThrownBy(() -> getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class, + INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::nonArrayValueAttributeRequirements); } @Test void inheritedAnnotationsWhenWrongComponentTypeThrowsException() { - assertThatAnnotationConfigurationException().isThrownBy(() -> - getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class, - InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, getClass())) - .satisfies(this::wrongComponentTypeRequirements); + assertThatAnnotationConfigurationException() + .isThrownBy(() -> getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class, + InvalidRepeatable.class, INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::wrongComponentTypeRequirements); } @Test void inheritedAnnotationsWhenOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.INHERITED_ANNOTATIONS, RepeatableClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + INHERITED_ANNOTATIONS, RepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void inheritedAnnotationsWhenWhenOnSuperclassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.INHERITED_ANNOTATIONS, SubRepeatableClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + INHERITED_ANNOTATIONS, SubRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void inheritedAnnotationsWhenComposedOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.INHERITED_ANNOTATIONS, ComposedRepeatableClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + INHERITED_ANNOTATIONS, ComposedRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void inheritedAnnotationsWhenComposedMixedWithContainerOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.INHERITED_ANNOTATIONS, - ComposedRepeatableMixedWithContainerClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + INHERITED_ANNOTATIONS, ComposedRepeatableMixedWithContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void inheritedAnnotationsWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.INHERITED_ANNOTATIONS, ComposedContainerClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + INHERITED_ANNOTATIONS, ComposedContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, Noninherited.class, - SearchStrategy.INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class); - assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", - "B", "C"); + INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", "B", "C"); } @Test void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { Set annotations = getAnnotations(null, Noninherited.class, - SearchStrategy.INHERITED_ANNOTATIONS, - SubNoninheritedRepeatableClass.class); + INHERITED_ANNOTATIONS, SubNoninheritedRepeatableClass.class); assertThat(annotations).isEmpty(); } @Test void typeHierarchyWhenNonRepeatableThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> - getAnnotations(null, NonRepeatable.class, SearchStrategy.TYPE_HIERARCHY, getClass())) - .satisfies(this::nonRepeatableRequirements); + assertThatIllegalArgumentException() + .isThrownBy(() -> getAnnotations(null, NonRepeatable.class, TYPE_HIERARCHY, getClass())) + .satisfies(this::nonRepeatableRequirements); } @Test void typeHierarchyWhenContainerMissingValueAttributeThrowsException() { - assertThatAnnotationConfigurationException().isThrownBy(() -> - getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class, - SearchStrategy.TYPE_HIERARCHY, getClass())) - .satisfies(this::missingValueAttributeRequirements); + assertThatAnnotationConfigurationException() + .isThrownBy(() -> getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class, + TYPE_HIERARCHY, getClass())) + .satisfies(this::missingValueAttributeRequirements); } @Test void typeHierarchyWhenWhenNonArrayValueAttributeThrowsException() { - assertThatAnnotationConfigurationException().isThrownBy(() -> - getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class, - SearchStrategy.TYPE_HIERARCHY, getClass())) - .satisfies(this::nonArrayValueAttributeRequirements); + assertThatAnnotationConfigurationException() + .isThrownBy(() -> getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class, + TYPE_HIERARCHY, getClass())) + .satisfies(this::nonArrayValueAttributeRequirements); } @Test void typeHierarchyWhenWrongComponentTypeThrowsException() { - assertThatAnnotationConfigurationException().isThrownBy(() -> - getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class, - InvalidRepeatable.class, SearchStrategy.TYPE_HIERARCHY, getClass())) - .satisfies(this::wrongComponentTypeRequirements); + assertThatAnnotationConfigurationException() + .isThrownBy(() -> getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class, + InvalidRepeatable.class, TYPE_HIERARCHY, getClass())) + .satisfies(this::wrongComponentTypeRequirements); } @Test void typeHierarchyWhenOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.TYPE_HIERARCHY, RepeatableClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + TYPE_HIERARCHY, RepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test - void typeHierarchyWhenWhenOnSuperclassReturnsAnnotations() { + void typeHierarchyWhenOnSuperclassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.TYPE_HIERARCHY, SubRepeatableClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + TYPE_HIERARCHY, SubRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void typeHierarchyWhenComposedOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.TYPE_HIERARCHY, ComposedRepeatableClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + TYPE_HIERARCHY, ComposedRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void typeHierarchyWhenComposedMixedWithContainerOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.TYPE_HIERARCHY, - ComposedRepeatableMixedWithContainerClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + TYPE_HIERARCHY, ComposedRepeatableMixedWithContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void typeHierarchyWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, - SearchStrategy.TYPE_HIERARCHY, ComposedContainerClass.class); - assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", - "C"); + TYPE_HIERARCHY, ComposedContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C"); } @Test void typeHierarchyAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { Set annotations = getAnnotations(null, Noninherited.class, - SearchStrategy.TYPE_HIERARCHY, NoninheritedRepeatableClass.class); - assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", - "B", "C"); + TYPE_HIERARCHY, NoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", "B", "C"); } @Test void typeHierarchyAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { Set annotations = getAnnotations(null, Noninherited.class, - SearchStrategy.TYPE_HIERARCHY, SubNoninheritedRepeatableClass.class); - assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", - "B", "C"); + TYPE_HIERARCHY, SubNoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", "B", "C"); } @Test void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnotationsAreFiltered() { Class element = WithRepeatedMetaAnnotationsClass.class; - SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY; + SearchStrategy searchStrategy = TYPE_HIERARCHY; AnnotationFilter annotationFilter = PeteRepeat.class.getName()::equals; Set annotations = getAnnotations(null, PeteRepeat.class, searchStrategy, element, annotationFilter); @@ -240,6 +227,44 @@ void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnot assertThat(annotationTypes).containsExactly(WithRepeatedMetaAnnotations.class, Noninherited.class, Noninherited.class); } + @Test // gh-32731 + void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() { + Class clazz = StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class; + + // NO RepeatableContainers + MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.none()); + ContainerWithMultipleAttributes container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + RepeatableWithContainerWithMultipleAttributes[] repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + Set set = + mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Only finds the locally declared repeated annotation. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("C"); + + // Standard RepeatableContainers + mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.standardRepeatables()); + container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + set = mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Finds the locally declared repeated annotation plus the 2 in the container. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B", "C"); + } + private Set getAnnotations(Class container, Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element) { @@ -255,32 +280,37 @@ private Set getAnnotations(Class } private void nonRepeatableRequirements(Exception ex) { - assertThat(ex.getMessage()).startsWith( - "Annotation type must be a repeatable annotation").contains( - "failed to resolve container type for", - NonRepeatable.class.getName()); + assertThat(ex) + .hasMessageStartingWith("Annotation type must be a repeatable annotation") + .hasMessageContaining("failed to resolve container type for", NonRepeatable.class.getName()); } private void missingValueAttributeRequirements(Exception ex) { - assertThat(ex.getMessage()).startsWith( - "Invalid declaration of container type").contains( + assertThat(ex) + .hasMessageStartingWith("Invalid declaration of container type") + .hasMessageContaining( ContainerMissingValueAttribute.class.getName(), - "for repeatable annotation", InvalidRepeatable.class.getName()); - assertThat(ex).hasCauseInstanceOf(NoSuchMethodException.class); + "for repeatable annotation", + InvalidRepeatable.class.getName()) + .hasCauseInstanceOf(NoSuchMethodException.class); } private void nonArrayValueAttributeRequirements(Exception ex) { - assertThat(ex.getMessage()).startsWith("Container type").contains( - ContainerWithNonArrayValueAttribute.class.getName(), - "must declare a 'value' attribute for an array of type", - InvalidRepeatable.class.getName()); + assertThat(ex) + .hasMessageStartingWith("Container type") + .hasMessageContaining( + ContainerWithNonArrayValueAttribute.class.getName(), + "must declare a 'value' attribute for an array of type", + InvalidRepeatable.class.getName()); } private void wrongComponentTypeRequirements(Exception ex) { - assertThat(ex.getMessage()).startsWith("Container type").contains( - ContainerWithArrayValueAttributeButWrongComponentType.class.getName(), - "must declare a 'value' attribute for an array of type", - InvalidRepeatable.class.getName()); + assertThat(ex) + .hasMessageStartingWith("Container type") + .hasMessageContaining( + ContainerWithArrayValueAttributeButWrongComponentType.class.getName(), + "must declare a 'value' attribute for an array of type", + InvalidRepeatable.class.getName()); } private static ThrowableTypeAssert assertThatAnnotationConfigurationException() { @@ -289,33 +319,28 @@ private static ThrowableTypeAssert assertThatA @Retention(RetentionPolicy.RUNTIME) @interface NonRepeatable { - } @Retention(RetentionPolicy.RUNTIME) @interface ContainerMissingValueAttribute { // InvalidRepeatable[] value(); - } @Retention(RetentionPolicy.RUNTIME) @interface ContainerWithNonArrayValueAttribute { InvalidRepeatable value(); - } @Retention(RetentionPolicy.RUNTIME) @interface ContainerWithArrayValueAttributeButWrongComponentType { String[] value(); - } @Retention(RetentionPolicy.RUNTIME) @interface InvalidRepeatable { - } @Retention(RetentionPolicy.RUNTIME) @@ -323,7 +348,6 @@ private static ThrowableTypeAssert assertThatA @interface PeteRepeats { PeteRepeat[] value(); - } @Retention(RetentionPolicy.RUNTIME) @@ -332,7 +356,6 @@ private static ThrowableTypeAssert assertThatA @interface PeteRepeat { String value(); - } @PeteRepeat("shadowed") @@ -343,7 +366,6 @@ private static ThrowableTypeAssert assertThatA @AliasFor(annotation = PeteRepeat.class) String value(); - } @PeteRepeat("shadowed") @@ -354,7 +376,6 @@ private static ThrowableTypeAssert assertThatA @AliasFor(annotation = PeteRepeat.class) String value(); - } @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) @@ -362,37 +383,31 @@ private static ThrowableTypeAssert assertThatA @Retention(RetentionPolicy.RUNTIME) @Inherited @interface ComposedContainer { - } @PeteRepeat("A") @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) static class RepeatableClass { - } static class SubRepeatableClass extends RepeatableClass { - } @ForPetesSake("B") @ForTheLoveOfFoo("C") @PeteRepeat("A") static class ComposedRepeatableClass { - } @ForPetesSake("C") @PeteRepeats(@PeteRepeat("A")) @PeteRepeat("B") static class ComposedRepeatableMixedWithContainerClass { - } @PeteRepeat("A") @ComposedContainer static class ComposedContainerClass { - } @Target(ElementType.TYPE) @@ -400,7 +415,6 @@ static class ComposedContainerClass { @interface Noninheriteds { Noninherited[] value(); - } @Target(ElementType.TYPE) @@ -413,7 +427,6 @@ static class ComposedContainerClass { @AliasFor("value") String name() default ""; - } @Noninherited(name = "shadowed") @@ -423,17 +436,14 @@ static class ComposedContainerClass { @AliasFor(annotation = Noninherited.class) String name() default ""; - } @ComposedNoninherited(name = "C") @Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") }) static class NoninheritedRepeatableClass { - } static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass { - } @Retention(RetentionPolicy.RUNTIME) @@ -449,4 +459,27 @@ static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass static class WithRepeatedMetaAnnotationsClass { } + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithMultipleAttributes { + + RepeatableWithContainerWithMultipleAttributes[] value(); + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(ContainerWithMultipleAttributes.class) + @interface RepeatableWithContainerWithMultipleAttributes { + + String value() default ""; + } + + @ContainerWithMultipleAttributes(name = "enigma", value = { + @RepeatableWithContainerWithMultipleAttributes("A"), + @RepeatableWithContainerWithMultipleAttributes("B") + }) + @RepeatableWithContainerWithMultipleAttributes("C") + static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase { + } + } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 8ffd0eeb8939..90dbbe9478ef 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,8 @@ import org.junit.jupiter.api.Test; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationsScannerTests.Hello2Impl; +import org.springframework.core.annotation.AnnotationsScannerTests.TestAnnotation1; import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotations.Search; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; @@ -86,47 +88,47 @@ class FluentSearchApiTests { @Test void preconditions() { assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.search(null)) - .withMessage("SearchStrategy must not be null"); + .isThrownBy(() -> MergedAnnotations.search(null)) + .withMessage("SearchStrategy must not be null"); Search search = MergedAnnotations.search(SearchStrategy.SUPERCLASS); assertThatIllegalArgumentException() - .isThrownBy(() -> search.withEnclosingClasses(null)) - .withMessage("Predicate must not be null"); + .isThrownBy(() -> search.withEnclosingClasses(null)) + .withMessage("Predicate must not be null"); assertThatIllegalStateException() - .isThrownBy(() -> search.withEnclosingClasses(Search.always)) - .withMessage("A custom 'searchEnclosingClass' predicate can only be combined with SearchStrategy.TYPE_HIERARCHY"); + .isThrownBy(() -> search.withEnclosingClasses(Search.always)) + .withMessage("A custom 'searchEnclosingClass' predicate can only be combined with SearchStrategy.TYPE_HIERARCHY"); assertThatIllegalArgumentException() - .isThrownBy(() -> search.withAnnotationFilter(null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> search.withAnnotationFilter(null)) + .withMessage("AnnotationFilter must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> search.withRepeatableContainers(null)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> search.withRepeatableContainers(null)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> search.from(null)) - .withMessage("AnnotatedElement must not be null"); + .isThrownBy(() -> search.from(null)) + .withMessage("AnnotatedElement must not be null"); } @Test void searchFromClassWithDefaultAnnotationFilterAndDefaultRepeatableContainers() { Stream> classes = MergedAnnotations.search(SearchStrategy.DIRECT) - .from(TransactionalComponent.class) - .stream() - .map(MergedAnnotation::getType); + .from(TransactionalComponent.class) + .stream() + .map(MergedAnnotation::getType); assertThat(classes).containsExactly(Transactional.class, Component.class, Indexed.class); } @Test void searchFromClassWithCustomAnnotationFilter() { Stream> classes = MergedAnnotations.search(SearchStrategy.DIRECT) - .withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed")) - .from(TransactionalComponent.class) - .stream() - .map(MergedAnnotation::getType); + .withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed")) + .from(TransactionalComponent.class) + .stream() + .map(MergedAnnotation::getType); assertThat(classes).containsExactly(Transactional.class, Component.class); } @@ -136,14 +138,14 @@ void searchFromClassWithCustomRepeatableContainers() { RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class); MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT) - .withRepeatableContainers(containers) - .from(HierarchyClass.class); + .withRepeatableContainers(containers) + .from(HierarchyClass.class); assertThat(annotations.stream(TestConfiguration.class)) - .map(annotation -> annotation.getString("location")) - .containsExactly("A", "B"); + .map(annotation -> annotation.getString("location")) + .containsExactly("A", "B"); assertThat(annotations.stream(TestConfiguration.class)) - .map(annotation -> annotation.getString("value")) - .containsExactly("A", "B"); + .map(annotation -> annotation.getString("value")) + .containsExactly("A", "B"); } /** @@ -203,9 +205,9 @@ void searchFromNonAnnotatedStaticNestedClassWithAnnotatedEnclosingClassWithEnclo .map(MergedAnnotation::getType); assertThat(classes).containsExactly(Component.class, Indexed.class); } - } + @Nested class ConventionBasedAnnotationAttributeOverrideTests { @@ -213,7 +215,7 @@ class ConventionBasedAnnotationAttributeOverrideTests { void getWithInheritedAnnotationsAttributesWithConventionBasedComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from(ConventionBasedComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); @@ -225,7 +227,7 @@ void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnn // xmlConfigFiles can be used because it has an AliasFor annotation MergedAnnotation annotation = MergedAnnotations.from(HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); } @@ -236,7 +238,7 @@ void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnn // locations doesn't apply because it has no AliasFor annotation MergedAnnotation annotation = MergedAnnotations.from(HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass2.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); } @@ -257,7 +259,7 @@ void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaConvention() { void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { MergedAnnotation annotation = MergedAnnotations.from(SpringApplicationConfigurationClass.class, SearchStrategy.TYPE_HIERARCHY) - .get(ContextConfiguration.class); + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); assertThat(annotation.getClassArray("classes")).containsExactly(Number.class); @@ -267,33 +269,32 @@ void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotat void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaConvention() throws Exception { testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); } - } + @Test void fromPreconditions() { SearchStrategy strategy = SearchStrategy.DIRECT; RepeatableContainers containers = RepeatableContainers.standardRepeatables(); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, null, AnnotationFilter.PLAIN)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, containers, null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, containers, null)) + .withMessage("AnnotationFilter must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], null, AnnotationFilter.PLAIN)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], containers, null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], containers, null)) + .withMessage("AnnotationFilter must not be null"); } @Test void streamWhenFromNonAnnotatedClass() { - assertThat(MergedAnnotations.from(NonAnnotatedClass.class). - stream(TransactionalComponent.class)).isEmpty(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).stream(TransactionalComponent.class)).isEmpty(); } @Test @@ -313,14 +314,12 @@ void streamWhenFromClassWithMetaDepth2() { @Test void isPresentWhenFromNonAnnotatedClass() { - assertThat(MergedAnnotations.from(NonAnnotatedClass.class). - isPresent(Transactional.class)).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).isPresent(Transactional.class)).isFalse(); } @Test void isPresentWhenFromAnnotationClassWithMetaDepth0() { - assertThat(MergedAnnotations.from(TransactionalComponent.class). - isPresent(TransactionalComponent.class)).isFalse(); + assertThat(MergedAnnotations.from(TransactionalComponent.class).isPresent(TransactionalComponent.class)).isFalse(); } @Test @@ -332,8 +331,7 @@ void isPresentWhenFromAnnotationClassWithMetaDepth1() { @Test void isPresentWhenFromAnnotationClassWithMetaDepth2() { - MergedAnnotations annotations = MergedAnnotations.from( - ComposedTransactionalComponent.class); + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponent.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isFalse(); @@ -341,28 +339,24 @@ void isPresentWhenFromAnnotationClassWithMetaDepth2() { @Test void isPresentWhenFromClassWithMetaDepth0() { - assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent( - TransactionalComponent.class)).isTrue(); + assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent(TransactionalComponent.class)).isTrue(); } @Test void isPresentWhenFromSubclassWithMetaDepth0() { - assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent( - TransactionalComponent.class)).isFalse(); + assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent(TransactionalComponent.class)).isFalse(); } @Test void isPresentWhenFromClassWithMetaDepth1() { - MergedAnnotations annotations = MergedAnnotations.from( - TransactionalComponentClass.class); + MergedAnnotations annotations = MergedAnnotations.from(TransactionalComponentClass.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); } @Test void isPresentWhenFromClassWithMetaDepth2() { - MergedAnnotations annotations = MergedAnnotations.from( - ComposedTransactionalComponentClass.class); + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponentClass.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isTrue(); @@ -393,55 +387,48 @@ void getRootWhenDirect() { @Test void getMetaTypes() { - MergedAnnotation annotation = MergedAnnotations.from( - ComposedTransactionalComponentClass.class).get( - TransactionalComponent.class); + MergedAnnotation annotation = MergedAnnotations.from(ComposedTransactionalComponentClass.class) + .get(TransactionalComponent.class); assertThat(annotation.getMetaTypes()).containsExactly( ComposedTransactionalComponent.class, TransactionalComponent.class); } @Test void collectMultiValueMapFromNonAnnotatedClass() { - MultiValueMap map = MergedAnnotations.from( - NonAnnotatedClass.class).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + MultiValueMap map = MergedAnnotations.from(NonAnnotatedClass.class) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).isEmpty(); } @Test void collectMultiValueMapFromClassWithLocalAnnotation() { - MultiValueMap map = MergedAnnotations.from(TxConfig.class).stream( - Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("value", Arrays.asList("TxConfig"))); + MultiValueMap map = MergedAnnotations.from(TxConfig.class) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", List.of("TxConfig"))); } @Test void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotation() { MultiValueMap map = MergedAnnotations.from( - SubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains( - entry("qualifier", Arrays.asList("composed2", "transactionManager"))); + SubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", List.of("composed2", "transactionManager"))); } @Test void collectMultiValueMapFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - SubSubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("qualifier", Arrays.asList("transactionManager"))); + SubSubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", List.of("transactionManager"))); } @Test void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - SubSubClassWithInheritedComposedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("qualifier", Arrays.asList("composed1"))); + SubSubClassWithInheritedComposedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", List.of("composed1"))); } /** @@ -453,10 +440,10 @@ void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclar */ @Test void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { - MultiValueMap map = MergedAnnotations.from(DerivedTxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("value", Arrays.asList("DerivedTxConfig"))); + MultiValueMap map = MergedAnnotations.from( + DerivedTxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", List.of("DerivedTxConfig"))); } /** @@ -466,24 +453,22 @@ void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSu @Test void collectMultiValueMapFromClassWithMultipleComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - TxFromMultipleComposedAnnotations.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains( - entry("value", Arrays.asList("TxInheritedComposed", "TxComposed"))); + TxFromMultipleComposedAnnotations.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", List.of("TxInheritedComposed", "TxComposed"))); } @Test void getWithInheritedAnnotationsFromClassWithLocalAnnotation() { - MergedAnnotation annotation = MergedAnnotations.from(TxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from( + TxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getString("value")).isEqualTo("TxConfig"); } @Test void getWithInheritedAnnotationsFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { - MergedAnnotation annotation = MergedAnnotations.from(DerivedTxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from( + DerivedTxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getString("value")).isEqualTo("DerivedTxConfig"); } @@ -497,53 +482,46 @@ void getWithInheritedAnnotationsFromMetaCycleAnnotatedClassWithMissingTargetMeta @Test void getWithInheritedAnnotationsFavorsLocalComposedAnnotationOverInheritedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - SubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isTrue(); } @Test void getWithInheritedAnnotationsFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubSubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isFalse(); } @Test void getWithInheritedAnnotationsFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubClassWithInheritedComposedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubSubClassWithInheritedComposedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isFalse(); } @Test void getWithInheritedAnnotationsFromInterfaceImplementedBySuperclass() { MergedAnnotation annotation = MergedAnnotations.from( - ConcreteClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + ConcreteClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.isPresent()).isFalse(); } @Test void getWithInheritedAnnotationsFromInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - InheritedAnnotationInterface.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + InheritedAnnotationInterface.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); } @Test void getWithInheritedAnnotationsFromNonInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - NonInheritedAnnotationInterface.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); + NonInheritedAnnotationInterface.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); assertThat(annotation.isPresent()).isTrue(); } - @Test void withInheritedAnnotationsFromAliasedComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( @@ -565,15 +543,11 @@ void withInheritedAnnotationsFromAliasedValueComposedAnnotation() { @Test void getWithInheritedAnnotationsFromImplicitAliasesInMetaAnnotationOnComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - ComposedImplicitAliasesContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get( - ImplicitAliasesContextConfiguration.class); - assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", - "B.xml"); - assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", - "B.xml"); - assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", - "B.xml"); + ComposedImplicitAliasesContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ImplicitAliasesContextConfiguration.class); + assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", "B.xml"); + assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", "B.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", "B.xml"); assertThat(annotation.getStringArray("value")).containsExactly("A.xml", "B.xml"); } @@ -613,8 +587,8 @@ void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSkippedLevelWit } private void testGetWithInherited(Class element, String... expected) { - MergedAnnotation annotation = MergedAnnotations.from(element, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEqualTo(expected); assertThat(annotation.getStringArray("value")).isEqualTo(expected); assertThat(annotation.getClassArray("classes")).isEmpty(); @@ -623,8 +597,8 @@ private void testGetWithInherited(Class element, String... expected) { @Test void getWithInheritedAnnotationsFromShadowedAliasComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - ShadowedAliasComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + ShadowedAliasComposedContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); } @@ -672,38 +646,39 @@ void getWithTypeHierarchyFromSubNonInheritedAnnotationInterface() { @Test void getWithTypeHierarchyFromSubSubNonInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubNonInheritedAnnotationInterface.class, - SearchStrategy.TYPE_HIERARCHY).get(Order.class); + SubSubNonInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Order.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(2); } @Test - void getWithTypeHierarchyInheritedFromInterfaceMethod() - throws NoSuchMethodException { - Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( - "handleFromInterface"); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Order.class); + void getWithTypeHierarchyInheritedFromInterfaceMethod() throws Exception { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleFromInterface"); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } + @Test // gh-31803 + void streamWithTypeHierarchyInheritedFromSuperInterfaceMethod() throws Exception { + Method method = Hello2Impl.class.getMethod("method"); + long count = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) + .from(method).stream(TestAnnotation1.class).count(); + assertThat(count).isEqualTo(1); + } + @Test void getWithTypeHierarchyInheritedFromAbstractMethod() throws NoSuchMethodException { Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handle"); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @Test void getWithTypeHierarchyInheritedFromBridgedMethod() throws NoSuchMethodException { - Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( - "handleParameterized", String.class); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleParameterized", String.class); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @@ -731,16 +706,14 @@ void getWithTypeHierarchyFromBridgeMethod() { @Test void getWithTypeHierarchyFromClassWithMetaAndLocalTxConfig() { MergedAnnotation annotation = MergedAnnotations.from( - MetaAndLocalTxConfigClass.class, SearchStrategy.TYPE_HIERARCHY).get( - Transactional.class); + MetaAndLocalTxConfigClass.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.getString("qualifier")).isEqualTo("localTxMgr"); } @Test void getWithTypeHierarchyFromClassWithAttributeAliasesInTargetAnnotation() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - AliasedTransactionalComponentClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + AliasedTransactionalComponentClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); AliasedTransactional synthesizedAnnotation = mergedAnnotation.synthesize(); String qualifier = "aliasForQualifier"; assertThat(mergedAnnotation.getString("value")).isEqualTo(qualifier); @@ -752,8 +725,7 @@ void getWithTypeHierarchyFromClassWithAttributeAliasesInTargetAnnotation() { @Test // gh-23767 void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); assertThat(mergedAnnotation.getString("value")).isEqualTo("anotherTransactionManager"); assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("anotherTransactionManager"); } @@ -761,8 +733,7 @@ void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() { @Test // gh-23767 void getWithTypeHierarchyFromClassWithMetaMetaAliasedTransactional() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); assertThat(mergedAnnotation.getString("value")).isEqualTo("meta"); assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("meta"); } @@ -798,61 +769,55 @@ private MergedAnnotation testGetWithTypeHierarchy(Class element, String... @Test void getWithTypeHierarchyWhenMultipleMetaAnnotationsHaveClashingAttributeNames() { MergedAnnotations annotations = MergedAnnotations.from( - AliasedComposedContextConfigurationAndTestPropertySourceClass.class, - SearchStrategy.TYPE_HIERARCHY); + AliasedComposedContextConfigurationAndTestPropertySourceClass.class, SearchStrategy.TYPE_HIERARCHY); MergedAnnotation contextConfig = annotations.get(ContextConfiguration.class); assertThat(contextConfig.getStringArray("locations")).containsExactly("test.xml"); assertThat(contextConfig.getStringArray("value")).containsExactly("test.xml"); MergedAnnotation testPropSource = annotations.get(TestPropertySource.class); - assertThat(testPropSource.getStringArray("locations")).containsExactly( - "test.properties"); - assertThat(testPropSource.getStringArray("value")).containsExactly( - "test.properties"); + assertThat(testPropSource.getStringArray("locations")).containsExactly("test.properties"); + assertThat(testPropSource.getStringArray("value")).containsExactly("test.properties"); } @Test void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { - testGetWithTypeHierarchyWebMapping( - WebController.class.getMethod("getMappedWithValueAttribute")); - testGetWithTypeHierarchyWebMapping( - WebController.class.getMethod("getMappedWithPathAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); } private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element) { - MergedAnnotation annotation = MergedAnnotations.from(element, - SearchStrategy.TYPE_HIERARCHY).get(RequestMapping.class); + MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) + .get(RequestMapping.class); assertThat(annotation.getStringArray("value")).containsExactly("/test"); assertThat(annotation.getStringArray("path")).containsExactly("/test"); } @Test - void getDirectWithJavaxAnnotationType() throws Exception { - assertThat(MergedAnnotations.from(ResourceHolder.class).get( - Resource.class).getString("name")).isEqualTo("x"); + void getDirectWithJavaxAnnotationType() { + assertThat(MergedAnnotations.from(ResourceHolder.class).get(Resource.class) + .getString("name")).isEqualTo("x"); } @Test void streamInheritedFromClassWithInterface() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); - assertThat(MergedAnnotations.from(method, SearchStrategy.INHERITED_ANNOTATIONS).stream( - Transactional.class)).isEmpty(); + assertThat(MergedAnnotations.from(method, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class)).isEmpty(); } @Test void streamTypeHierarchyFromClassWithInterface() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).stream( - Transactional.class)).hasSize(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) + .stream(Transactional.class)).hasSize(1); } @Test - @SuppressWarnings("deprecation") void getFromMethodWithMethodAnnotationOnLeaf() throws Exception { Method method = Leaf.class.getMethod("annotatedOnLeaf"); assertThat(method.getAnnotation(Order.class)).isNotNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(0); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(0); } @Test @@ -860,8 +825,8 @@ void getFromMethodWithAnnotationOnMethodInInterface() throws Exception { Method method = Leaf.class.getMethod("fromInterfaceImplementedByRoot"); assertThat(method.getAnnotation(Order.class)).isNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(0); } @Test @@ -869,8 +834,8 @@ void getFromMethodWithMetaAnnotationOnLeaf() throws Exception { Method method = Leaf.class.getMethod("metaAnnotatedOnLeaf"); assertThat(method.getAnnotation(Order.class)).isNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(1); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(1); } @Test @@ -1162,7 +1127,7 @@ void getSuperClassSourceForTypesWithSingleCandidateType() { @Test void getSuperClassSourceForTypesWithMultipleCandidateTypes() { - List> candidates = Arrays.asList(Transactional.class, Order.class); + List> candidates = List.of(Transactional.class, Order.class); // no class-level annotation assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedInterface.class, candidates)).isNull(); @@ -1205,7 +1170,7 @@ private Object getSuperClassSourceWithTypeIn(Class clazz, List> annotations = MergedAnnotations.from( method, SearchStrategy.TYPE_HIERARCHY).stream(MyRepeatable.class); @@ -1395,7 +1360,7 @@ void getRepeatableDeclaredOnMethod() throws Exception { @Test @SuppressWarnings("deprecation") - void getRepeatableDeclaredOnClassWithAttributeAliases() { + void streamRepeatableDeclaredOnClassWithAttributeAliases() { assertThat(MergedAnnotations.from(HierarchyClass.class).stream( TestConfiguration.class)).isEmpty(); RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, @@ -1409,7 +1374,7 @@ void getRepeatableDeclaredOnClassWithAttributeAliases() { } @Test - void getRepeatableDeclaredOnClass() { + void streamRepeatableDeclaredOnClass() { Class element = MyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1417,7 +1382,7 @@ void getRepeatableDeclaredOnClass() { } @Test - void getRepeatableDeclaredOnSuperclass() { + void streamRepeatableDeclaredOnSuperclass() { Class element = SubMyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1425,7 +1390,7 @@ void getRepeatableDeclaredOnSuperclass() { } @Test - void getRepeatableDeclaredOnClassAndSuperclass() { + void streamRepeatableDeclaredOnClassAndSuperclass() { Class element = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class; String[] expectedValuesJava = { "X", "Y", "Z" }; String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; @@ -1433,7 +1398,7 @@ void getRepeatableDeclaredOnClassAndSuperclass() { } @Test - void getRepeatableDeclaredOnMultipleSuperclasses() { + void streamRepeatableDeclaredOnMultipleSuperclasses() { Class element = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class; String[] expectedValuesJava = { "X", "Y", "Z" }; String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; @@ -1441,7 +1406,7 @@ void getRepeatableDeclaredOnMultipleSuperclasses() { } @Test - void getDirectRepeatablesDeclaredOnClass() { + void streamDirectRepeatablesDeclaredOnClass() { Class element = MyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1449,7 +1414,7 @@ void getDirectRepeatablesDeclaredOnClass() { } @Test - void getDirectRepeatablesDeclaredOnSuperclass() { + void streamDirectRepeatablesDeclaredOnSuperclass() { Class element = SubMyRepeatableClass.class; String[] expectedValuesJava = {}; String[] expectedValuesSpring = {}; @@ -1476,24 +1441,21 @@ private void testExplicitRepeatables(SearchStrategy searchStrategy, Class ele MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class), AnnotationFilter.PLAIN); - assertThat(annotations.stream(MyRepeatable.class).filter( - MergedAnnotationPredicates.firstRunOf( - MergedAnnotation::getAggregateIndex)).map( - annotation -> annotation.getString( - "value"))).containsExactly(expected); + Stream values = annotations.stream(MyRepeatable.class) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(annotation -> annotation.getString("value")); + assertThat(values).containsExactly(expected); } private void testStandardRepeatables(SearchStrategy searchStrategy, Class element, String[] expected) { - MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy); - assertThat(annotations.stream(MyRepeatable.class).filter( - MergedAnnotationPredicates.firstRunOf( - MergedAnnotation::getAggregateIndex)).map( - annotation -> annotation.getString( - "value"))).containsExactly(expected); + Stream values = MergedAnnotations.from(element, searchStrategy).stream(MyRepeatable.class) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(annotation -> annotation.getString("value")); + assertThat(values).containsExactly(expected); } @Test - void synthesizeWithoutAttributeAliases() throws Exception { + void synthesizeWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Component synthesizedComponent = MergedAnnotation.from(component).synthesize(); @@ -1625,114 +1587,123 @@ void synthesizeShouldNotResynthesizeAlreadySynthesizedAnnotations() throws Excep } @Test - void synthesizeWhenAliasForIsMissingAttributeDeclaration() throws Exception { + void synthesizeWhenAliasForIsMissingAttributeDeclaration() { AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation( AliasForWithMissingAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") - .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) - .withMessageContaining("points to itself"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) + .withMessageContaining("points to itself"); } @Test - void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() throws Exception { - AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( - AliasForWithDuplicateAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") - .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) - .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); + void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() { + AliasForWithDuplicateAttributeDeclaration annotation = + AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( + AliasForWithDuplicateAttributeDeclaration.class); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) + .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); } @Test - void synthesizeWhenAttributeAliasForNonexistentAttribute() throws Exception { + void synthesizeWhenAttributeAliasForNonexistentAttribute() { AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation( AliasForNonexistentAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") - .withMessageContaining(AliasForNonexistentAttribute.class.getName()) - .withMessageContaining("declares an alias for 'bar' which is not present"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForNonexistentAttribute.class.getName()) + .withMessageContaining("declares an alias for 'bar' which is not present"); } @Test - void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() throws Exception { + void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() { AliasForWithMirroredAliasForWrongAttribute annotation = AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation( AliasForWithMirroredAliasForWrongAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" - + AliasForWithMirroredAliasForWrongAttribute.class.getName() - + "] declares an alias for 'quux' which is not present."); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" + + AliasForWithMirroredAliasForWrongAttribute.class.getName() + + "] declares an alias for 'quux' which is not present."); } @Test - void synthesizeWhenAttributeAliasForAttributeOfDifferentType() throws Exception { + void synthesizeWhenAttributeAliasForAttributeOfDifferentType() { AliasForAttributeOfDifferentType annotation = AliasForAttributeOfDifferentTypeClass.class.getAnnotation( AliasForAttributeOfDifferentType.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForAttributeOfDifferentType.class.getName()) - .withMessageContaining("attribute 'foo'") - .withMessageContaining("attribute 'bar'") - .withMessageContaining("same return type"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeOfDifferentType.class.getName()) + .withMessageContaining("attribute 'foo'") + .withMessageContaining("attribute 'bar'") + .withMessageContaining("same return type"); } @Test - void synthesizeWhenAttributeAliasForWithMissingDefaultValues() throws Exception { + void synthesizeWhenAttributeAliasForWithMissingDefaultValues() { AliasForWithMissingDefaultValues annotation = AliasForWithMissingDefaultValuesClass.class.getAnnotation( AliasForWithMissingDefaultValues.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForWithMissingDefaultValues.class.getName()) - .withMessageContaining("attribute 'foo' in annotation") - .withMessageContaining("attribute 'bar' in annotation") - .withMessageContaining("default values"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForWithMissingDefaultValues.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("default values"); } @Test - void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() throws Exception { + void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() { AliasForAttributeWithDifferentDefaultValue annotation = AliasForAttributeWithDifferentDefaultValueClass.class.getAnnotation( AliasForAttributeWithDifferentDefaultValue.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName()) - .withMessageContaining("attribute 'foo' in annotation") - .withMessageContaining("attribute 'bar' in annotation") - .withMessageContaining("same default value"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("same default value"); } @Test - void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() throws Exception { + void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() { AliasedComposedTestConfigurationNotMetaPresent annotation = AliasedComposedTestConfigurationNotMetaPresentClass.class.getAnnotation( AliasedComposedTestConfigurationNotMetaPresent.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") - .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) - .withMessageContaining("declares an alias for attribute 'location' in annotation") - .withMessageContaining(TestConfiguration.class.getName()) - .withMessageContaining("not meta-present"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") + .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) + .withMessageContaining("declares an alias for attribute 'location' in annotation") + .withMessageContaining(TestConfiguration.class.getName()) + .withMessageContaining("not meta-present"); } @Test - void synthesizeWithImplicitAliases() throws Exception { + void synthesizeWithImplicitAliases() { testSynthesisWithImplicitAliases(ValueImplicitAliasesTestConfigurationClass.class, "value"); testSynthesisWithImplicitAliases(Location1ImplicitAliasesTestConfigurationClass.class, "location1"); testSynthesisWithImplicitAliases(XmlImplicitAliasesTestConfigurationClass.class, "xmlFile"); testSynthesisWithImplicitAliases(GroovyImplicitAliasesSimpleTestConfigurationClass.class, "groovyScript"); } - private void testSynthesisWithImplicitAliases(Class clazz, String expected) throws Exception { + private void testSynthesisWithImplicitAliases(Class clazz, String expected) { ImplicitAliasesTestConfiguration config = clazz.getAnnotation(ImplicitAliasesTestConfiguration.class); assertThat(config).isNotNull(); ImplicitAliasesTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); @@ -1744,8 +1715,7 @@ private void testSynthesisWithImplicitAliases(Class clazz, String expected) t } @Test - void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() - throws Exception { + void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() { testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( ValueImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, "value"); @@ -1757,8 +1727,7 @@ void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() "xmlFile"); } - private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( - Class clazz, String expected) { + private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted(Class clazz, String expected) { ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration config = clazz.getAnnotation( ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class); assertThat(config).isNotNull(); @@ -1771,7 +1740,7 @@ private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( } @Test - void synthesizeWithImplicitAliasesForAliasPair() throws Exception { + void synthesizeWithImplicitAliasesForAliasPair() { ImplicitAliasesForAliasPairTestConfiguration config = ImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( ImplicitAliasesForAliasPairTestConfiguration.class); @@ -1782,7 +1751,7 @@ void synthesizeWithImplicitAliasesForAliasPair() throws Exception { } @Test - void synthesizeWithTransitiveImplicitAliases() throws Exception { + void synthesizeWithTransitiveImplicitAliases() { TransitiveImplicitAliasesTestConfiguration config = TransitiveImplicitAliasesTestConfigurationClass.class.getAnnotation( TransitiveImplicitAliasesTestConfiguration.class); @@ -1793,70 +1762,69 @@ void synthesizeWithTransitiveImplicitAliases() throws Exception { } @Test - void synthesizeWithTransitiveImplicitAliasesForAliasPair() throws Exception { + void synthesizeWithTransitiveImplicitAliasesForAliasPair() { TransitiveImplicitAliasesForAliasPairTestConfiguration config = TransitiveImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( TransitiveImplicitAliasesForAliasPairTestConfiguration.class); - TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from( - config).synthesize(); + TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); assertSynthesized(synthesized); assertThat(synthesized.xml()).isEqualTo("test.xml"); assertThat(synthesized.groovy()).isEqualTo("test.xml"); } @Test - void synthesizeWithImplicitAliasesWithMissingDefaultValues() throws Exception { + void synthesizeWithImplicitAliasesWithMissingDefaultValues() { Class clazz = ImplicitAliasesWithMissingDefaultValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithMissingDefaultValuesTestConfiguration.class; - ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Misconfigured aliases:") - .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("default values"); + ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("default values"); } @Test - void synthesizeWithImplicitAliasesWithDifferentDefaultValues() - throws Exception { + void synthesizeWithImplicitAliasesWithDifferentDefaultValues() { Class clazz = ImplicitAliasesWithDifferentDefaultValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithDifferentDefaultValuesTestConfiguration.class; - ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Misconfigured aliases:") - .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("same default value"); + ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("same default value"); } @Test - void synthesizeWithImplicitAliasesWithDuplicateValues() throws Exception { + void synthesizeWithImplicitAliasesWithDuplicateValues() { Class clazz = ImplicitAliasesWithDuplicateValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithDuplicateValuesTestConfiguration.class; - ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Different @AliasFor mirror values for annotation") - .withMessageContaining(annotationType.getName()) - .withMessageContaining("declared on class") - .withMessageContaining(clazz.getName()) - .withMessageContaining("are declared with values of"); + ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Different @AliasFor mirror values for annotation") + .withMessageContaining(annotationType.getName()) + .withMessageContaining("declared on class") + .withMessageContaining(clazz.getName()) + .withMessageContaining("are declared with values of"); } @Test - void synthesizeFromMapWithoutAttributeAliases() throws Exception { + void synthesizeFromMapWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Map map = Collections.singletonMap("value", "webController"); MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); + Component synthesizedComponent = annotation.synthesize(); assertSynthesized(synthesizedComponent); assertThat(synthesizedComponent.value()).isEqualTo("webController"); @@ -1864,14 +1832,13 @@ void synthesizeFromMapWithoutAttributeAliases() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeFromMapWithNestedMap() throws Exception { + void synthesizeFromMapWithNestedMap() { ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation( ComponentScanSingleFilter.class); assertThat(componentScan).isNotNull(); assertThat(componentScan.value().pattern()).isEqualTo("*Foo"); Map map = MergedAnnotation.from(componentScan).asMap( - annotation -> new LinkedHashMap<>(), - Adapt.ANNOTATION_TO_MAP); + annotation -> new LinkedHashMap<>(), Adapt.ANNOTATION_TO_MAP); Map filterMap = (Map) map.get("value"); assertThat(filterMap.get("pattern")).isEqualTo("*Foo"); filterMap.put("pattern", "newFoo"); @@ -1885,13 +1852,11 @@ void synthesizeFromMapWithNestedMap() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { - ComponentScan componentScan = ComponentScanClass.class.getAnnotation( - ComponentScan.class); + void synthesizeFromMapWithNestedArrayOfMaps() { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class); assertThat(componentScan).isNotNull(); Map map = MergedAnnotation.from(componentScan).asMap( - annotation -> new LinkedHashMap<>(), - Adapt.ANNOTATION_TO_MAP); + annotation -> new LinkedHashMap<>(), Adapt.ANNOTATION_TO_MAP); Map[] filters = (Map[]) map.get("excludeFilters"); List patterns = Arrays.stream(filters).map( m -> (String) m.get("pattern")).toList(); @@ -1900,18 +1865,16 @@ void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { filters[0].put("enigma", 42); filters[1].put("pattern", "newBar"); filters[1].put("enigma", 42); - MergedAnnotation annotation = MergedAnnotation.of( - ComponentScan.class, map); + MergedAnnotation annotation = MergedAnnotation.of(ComponentScan.class, map); ComponentScan synthesizedComponentScan = annotation.synthesize(); assertSynthesized(synthesizedComponentScan); - assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map( - Filter::pattern)).containsExactly("newFoo", "newBar"); + assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map(Filter::pattern)) + .containsExactly("newFoo", "newBar"); } @Test - void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { - MergedAnnotation annotation = MergedAnnotation.of( - AnnotationWithDefaults.class); + void synthesizeFromDefaultsWithoutAttributeAliases() { + MergedAnnotation annotation = MergedAnnotation.of(AnnotationWithDefaults.class); AnnotationWithDefaults synthesized = annotation.synthesize(); assertThat(synthesized.text()).isEqualTo("enigma"); assertThat(synthesized.predicate()).isTrue(); @@ -1919,51 +1882,45 @@ void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { } @Test - void synthesizeFromDefaultsWithAttributeAliases() throws Exception { - MergedAnnotation annotation = MergedAnnotation.of( - TestConfiguration.class); + void synthesizeFromDefaultsWithAttributeAliases() { + MergedAnnotation annotation = MergedAnnotation.of(TestConfiguration.class); TestConfiguration synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEmpty(); assertThat(synthesized.location()).isEmpty(); } @Test - void synthesizeWhenAttributeAliasesWithDifferentValues() throws Exception { + void synthesizeWhenAttributeAliasesWithDifferentValues() { assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> MergedAnnotation.from(TestConfigurationMismatch.class.getAnnotation(TestConfiguration.class)).synthesize()); } @Test - void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() - throws Exception { + void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() { Map map = Collections.singletonMap("location", "test.xml"); - MergedAnnotation annotation = MergedAnnotation.of( - TestConfiguration.class, map); + MergedAnnotation annotation = MergedAnnotation.of(TestConfiguration.class, map); TestConfiguration synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEqualTo("test.xml"); assertThat(synthesized.location()).isEqualTo("test.xml"); } @Test - void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() - throws Exception { + void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() { synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( Collections.singletonMap("value", "/foo")); synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( Collections.singletonMap("path", "/foo")); } - private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( - Map map) { - MergedAnnotation annotation = MergedAnnotation.of(GetMapping.class, - map); + private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements(Map map) { + MergedAnnotation annotation = MergedAnnotation.of(GetMapping.class, map); GetMapping synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEqualTo("/foo"); assertThat(synthesized.path()).isEqualTo("/foo"); } @Test - void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { + void synthesizeFromMapWithImplicitAttributeAliases() { testSynthesisFromMapWithImplicitAliases("value"); testSynthesisFromMapWithImplicitAliases("location1"); testSynthesisFromMapWithImplicitAliases("location2"); @@ -1972,13 +1929,12 @@ void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { testSynthesisFromMapWithImplicitAliases("groovyScript"); } - private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) - throws Exception { - Map map = Collections.singletonMap(attributeNameAndValue, - attributeNameAndValue); + private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) { + Map map = Collections.singletonMap(attributeNameAndValue, attributeNameAndValue); MergedAnnotation annotation = MergedAnnotation.of( ImplicitAliasesTestConfiguration.class, map); ImplicitAliasesTestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo(attributeNameAndValue); assertThat(synthesized.location1()).isEqualTo(attributeNameAndValue); assertThat(synthesized.location2()).isEqualTo(attributeNameAndValue); @@ -1988,12 +1944,12 @@ private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValu } @Test - void synthesizeFromMapWithMissingAttributeValue() throws Exception { + void synthesizeFromMapWithMissingAttributeValue() { testMissingTextAttribute(Collections.emptyMap()); } @Test - void synthesizeFromMapWithNullAttributeValue() throws Exception { + void synthesizeFromMapWithNullAttributeValue() { Map map = Collections.singletonMap("text", null); assertThat(map).containsKey("text"); testMissingTextAttribute(map); @@ -2002,12 +1958,12 @@ void synthesizeFromMapWithNullAttributeValue() throws Exception { private void testMissingTextAttribute(Map attributes) { assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> MergedAnnotation.of(AnnotationWithoutDefaults.class, attributes).synthesize().text()) - .withMessage("No value found for attribute named 'text' in merged annotation " + - AnnotationWithoutDefaults.class.getName()); + .withMessage("No value found for attribute named 'text' in merged annotation " + + AnnotationWithoutDefaults.class.getName()); } @Test - void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { + void synthesizeFromMapWithAttributeOfIncorrectType() { Map map = Collections.singletonMap("value", 42L); MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); assertThatIllegalStateException().isThrownBy(() -> annotation.synthesize().value()) @@ -2017,10 +1973,11 @@ void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { } @Test - void synthesizeFromAnnotationAttributesWithoutAttributeAliases() throws Exception { + void synthesizeFromAnnotationAttributesWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Map attributes = MergedAnnotation.from(component).asMap(); + Component synthesized = MergedAnnotation.of(Component.class, attributes).synthesize(); assertSynthesized(synthesized); assertThat(synthesized).isEqualTo(component); @@ -2054,47 +2011,41 @@ void toStringForSynthesizedAnnotations() throws Exception { private void assertToStringForWebMappingWithPathAndValue(RequestMapping webMapping) { assertThat(webMapping.toString()) - .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(") - .contains( - // Strings - "value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", - // Characters - "ch='X'", "chars={'X'}", - // Enums - "method={GET, POST}", - // Classes - "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class", - "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}", - // Bytes - "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}", - // Shorts - "shortValue=9876", "shorts={9876}", - // Longs - "longValue=42L", "longs={42L}", - // Floats - "floatValue=3.14f", "floats={3.14f}", - // Doubles - "doubleValue=99.999d", "doubles={99.999d}" - ) - .endsWith(")"); + .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(") + .contains( + // Strings + "value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", + // Characters + "ch='X'", "chars={'X'}", + // Enums + "method={GET, POST}", + // Classes + "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class", + "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}", + // Bytes + "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}", + // Shorts + "shortValue=9876", "shorts={9876}", + // Longs + "longValue=42L", "longs={42L}", + // Floats + "floatValue=3.14f", "floats={3.14f}", + // Doubles + "doubleValue=99.999d", "doubles={99.999d}" + ) + .endsWith(")"); } @Test void equalsForSynthesizedAnnotations() throws Exception { - Method methodWithPath = WebController.class.getMethod( - "handleMappedWithPathAttribute"); - RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( - RequestMapping.class); + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class); assertThat(webMappingWithAliases).isNotNull(); - Method methodWithPathAndValue = WebController.class.getMethod( - "handleMappedWithSamePathAndValueAttributes"); - RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( - RequestMapping.class); + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class); assertThat(webMappingWithPathAndValue).isNotNull(); - RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( - webMappingWithAliases).synthesize(); - RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( - webMappingWithPathAndValue).synthesize(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize(); // Equality amongst standard annotations assertThat(webMappingWithAliases).isEqualTo(webMappingWithAliases); assertThat(webMappingWithPathAndValue).isEqualTo(webMappingWithPathAndValue); @@ -2116,51 +2067,33 @@ void equalsForSynthesizedAnnotations() throws Exception { @Test void hashCodeForSynthesizedAnnotations() throws Exception { - Method methodWithPath = WebController.class.getMethod( - "handleMappedWithPathAttribute"); - RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( - RequestMapping.class); + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class); assertThat(webMappingWithAliases).isNotNull(); - Method methodWithPathAndValue = WebController.class.getMethod( - "handleMappedWithSamePathAndValueAttributes"); - RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( - RequestMapping.class); + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class); assertThat(webMappingWithPathAndValue).isNotNull(); - RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( - webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize(); assertThat(synthesizedWebMapping1).isNotNull(); - RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( - webMappingWithPathAndValue).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize(); assertThat(synthesizedWebMapping2).isNotNull(); // Equality amongst standard annotations - assertThat(webMappingWithAliases.hashCode()).isEqualTo( - webMappingWithAliases.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( - webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isEqualTo(webMappingWithAliases.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo(webMappingWithPathAndValue.hashCode()); // Inequality amongst standard annotations - assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( - webMappingWithPathAndValue.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo( - webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo(webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo(webMappingWithAliases.hashCode()); // Equality amongst synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); - assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( - synthesizedWebMapping2.hashCode()); - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - synthesizedWebMapping2.hashCode()); - assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo(synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); // Equality between standard and synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - webMappingWithPathAndValue.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); // Inequality between standard and synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo( - webMappingWithAliases.hashCode()); - assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo(webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo(synthesizedWebMapping1.hashCode()); } /** @@ -2188,7 +2121,7 @@ void synthesizeNonPublicWithAttributeAliasesFromDifferentPackage() throws Except } @Test - void synthesizeWithArrayOfAnnotations() throws Exception { + void synthesizeWithArrayOfAnnotations() { Hierarchy hierarchy = HierarchyClass.class.getAnnotation(Hierarchy.class); assertThat(hierarchy).isNotNull(); Hierarchy synthesizedHierarchy = MergedAnnotation.from(hierarchy).synthesize(); @@ -2210,12 +2143,10 @@ void synthesizeWithArrayOfAnnotations() throws Exception { } @Test - void synthesizeWithArrayOfChars() throws Exception { - CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation( - CharsContainer.class); + void synthesizeWithArrayOfChars() { + CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation(CharsContainer.class); assertThat(charsContainer).isNotNull(); - CharsContainer synthesizedCharsContainer = MergedAnnotation.from( - charsContainer).synthesize(); + CharsContainer synthesizedCharsContainer = MergedAnnotation.from(charsContainer).synthesize(); assertSynthesized(synthesizedCharsContainer); char[] chars = synthesizedCharsContainer.chars(); assertThat(chars).containsExactly('x', 'y', 'z'); @@ -2228,53 +2159,50 @@ void synthesizeWithArrayOfChars() throws Exception { @Test void getValueWhenHasDefaultOverride() { - MergedAnnotation annotation = MergedAnnotations.from( - DefaultOverrideClass.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideClass.class) + .get(DefaultOverrideRoot.class); assertThat(annotation.getString("text")).isEqualTo("metameta"); } @Test // gh-22654 void getValueWhenHasDefaultOverrideWithImplicitAlias() { - MergedAnnotation annotation1 = MergedAnnotations.from( - DefaultOverrideImplicitAliasMetaClass1.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation1 = MergedAnnotations.from(DefaultOverrideImplicitAliasMetaClass1.class) + .get(DefaultOverrideRoot.class); assertThat(annotation1.getString("text")).isEqualTo("alias-meta-1"); - MergedAnnotation annotation2 = MergedAnnotations.from( - DefaultOverrideImplicitAliasMetaClass2.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation2 = MergedAnnotations.from(DefaultOverrideImplicitAliasMetaClass2.class) + .get(DefaultOverrideRoot.class); assertThat(annotation2.getString("text")).isEqualTo("alias-meta-2"); } @Test // gh-22654 void getValueWhenHasDefaultOverrideWithExplicitAlias() { - MergedAnnotation annotation = MergedAnnotations.from( - DefaultOverrideExplicitAliasRootMetaMetaClass.class).get( - DefaultOverrideExplicitAliasRoot.class); + MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideExplicitAliasRootMetaMetaClass.class) + .get(DefaultOverrideExplicitAliasRoot.class); assertThat(annotation.getString("text")).isEqualTo("meta"); assertThat(annotation.getString("value")).isEqualTo("meta"); } @Test // gh-22703 void getValueWhenThreeDeepMetaWithValue() { - MergedAnnotation annotation = MergedAnnotations.from( - ValueAttributeMetaMetaClass.class).get(ValueAttribute.class); - assertThat(annotation.getStringArray(MergedAnnotation.VALUE)).containsExactly( - "FromValueAttributeMeta"); + MergedAnnotation annotation = MergedAnnotations.from(ValueAttributeMetaMetaClass.class) + .get(ValueAttribute.class); + assertThat(annotation.getStringArray(MergedAnnotation.VALUE)).containsExactly("FromValueAttributeMeta"); } @Test void asAnnotationAttributesReturnsPopulatedAnnotationAttributes() { - MergedAnnotation annotation = MergedAnnotations.from( - SpringApplicationConfigurationClass.class).get( - SpringApplicationConfiguration.class); - AnnotationAttributes attributes = annotation.asAnnotationAttributes( - Adapt.CLASS_TO_STRING); - assertThat(attributes).containsEntry("classes", new String[] { Number.class.getName() }); + MergedAnnotation annotation = MergedAnnotations.from(SpringApplicationConfigurationClass.class) + .get(SpringApplicationConfiguration.class); + AnnotationAttributes attributes = annotation.asAnnotationAttributes(Adapt.CLASS_TO_STRING); + assertThat(attributes).containsEntry("classes", new String[] {Number.class.getName()}); assertThat(attributes.annotationType()).isEqualTo(SpringApplicationConfiguration.class); } + // @formatter:off + @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) - @Target({ ElementType.TYPE, ElementType.METHOD }) @Inherited @interface Transactional { @@ -2327,8 +2255,8 @@ static class ComposedTransactionalComponentClass { static class AliasedTransactionalComponentClass { } + @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) - @Target({ ElementType.TYPE, ElementType.METHOD }) @Inherited @interface AliasedTransactional { @@ -2695,7 +2623,7 @@ interface InterfaceWithInheritedAnnotation { void handleFromInterface(); } - static abstract class AbstractClassWithInheritedAnnotation + abstract static class AbstractClassWithInheritedAnnotation implements InterfaceWithInheritedAnnotation { @Transactional @@ -2999,7 +2927,7 @@ public void overrideWithoutNewAnnotation() { } } - public static abstract class SimpleGeneric { + public abstract static class SimpleGeneric { @Order(1) public abstract void something(T arg); @@ -3089,7 +3017,7 @@ public void foo(String t) { } } - public static abstract class BaseClassWithGenericAnnotatedMethod { + public abstract static class BaseClassWithGenericAnnotatedMethod { @Order abstract void foo(T t); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java index dffb0c2101c6..21b96c0de0b7 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -106,7 +106,7 @@ void getComposedPlusLocalAnnotationsOnMethod() throws Exception { @Test @Disabled("Disabled since some Java 8 updates handle the bridge method differently") - void getMultipleComposedAnnotationsOnBridgeMethod() throws Exception { + void getMultipleComposedAnnotationsOnBridgeMethod() { Set cacheables = getAllMergedAnnotations(getBridgeMethod(), Cacheable.class); assertThat(cacheables).isNotNull(); assertThat(cacheables).isEmpty(); @@ -178,7 +178,7 @@ void findComposedPlusLocalAnnotationsOnMethod() throws Exception { } @Test - void findMultipleComposedAnnotationsOnBridgeMethod() throws Exception { + void findMultipleComposedAnnotationsOnBridgeMethod() { assertFindAllMergedAnnotationsBehavior(getBridgeMethod()); } @@ -186,7 +186,7 @@ void findMultipleComposedAnnotationsOnBridgeMethod() throws Exception { * Bridge/bridged method setup code copied from * {@link org.springframework.core.BridgeMethodResolverTests#withGenericParameter()}. */ - Method getBridgeMethod() throws NoSuchMethodException { + Method getBridgeMethod() { Method[] methods = StringGenericParameter.class.getMethods(); Method bridgeMethod = null; Method bridgedMethod = null; diff --git a/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.java index 2f99db0018dc..6e20b5973615 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ * * @author Sam Brannen * @since 5.3.24 - * @see https://github.com/spring-projects/spring-framework/issues/20279 */ @SuppressWarnings("unused") class NestedRepeatableAnnotationsTests { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/SynthesizingMethodParameterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/SynthesizingMethodParameterTests.java index a1eea0913140..24090c49e25c 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/SynthesizingMethodParameterTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/SynthesizingMethodParameterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class SynthesizingMethodParameterTests { @BeforeEach void setUp() throws NoSuchMethodException { - method = getClass().getMethod("method", String.class, Long.TYPE); + method = getClass().getMethod("method", String.class, long.class); stringParameter = new SynthesizingMethodParameter(method, 0); longParameter = new SynthesizingMethodParameter(method, 1); intReturnType = new SynthesizingMethodParameter(method, -1); @@ -56,14 +56,14 @@ void equals() throws NoSuchMethodException { assertThat(longParameter).isEqualTo(longParameter); assertThat(intReturnType).isEqualTo(intReturnType); - assertThat(stringParameter.equals(longParameter)).isFalse(); - assertThat(stringParameter.equals(intReturnType)).isFalse(); - assertThat(longParameter.equals(stringParameter)).isFalse(); - assertThat(longParameter.equals(intReturnType)).isFalse(); - assertThat(intReturnType.equals(stringParameter)).isFalse(); - assertThat(intReturnType.equals(longParameter)).isFalse(); + assertThat(stringParameter).isNotEqualTo(longParameter); + assertThat(stringParameter).isNotEqualTo(intReturnType); + assertThat(longParameter).isNotEqualTo(stringParameter); + assertThat(longParameter).isNotEqualTo(intReturnType); + assertThat(intReturnType).isNotEqualTo(stringParameter); + assertThat(intReturnType).isNotEqualTo(longParameter); - Method method = getClass().getMethod("method", String.class, Long.TYPE); + Method method = getClass().getMethod("method", String.class, long.class); MethodParameter methodParameter = new SynthesizingMethodParameter(method, 0); assertThat(methodParameter).isEqualTo(stringParameter); assertThat(stringParameter).isEqualTo(methodParameter); @@ -83,7 +83,7 @@ void testHashCode() throws NoSuchMethodException { assertThat(longParameter.hashCode()).isEqualTo(longParameter.hashCode()); assertThat(intReturnType.hashCode()).isEqualTo(intReturnType.hashCode()); - Method method = getClass().getMethod("method", String.class, Long.TYPE); + Method method = getClass().getMethod("method", String.class, long.class); SynthesizingMethodParameter methodParameter = new SynthesizingMethodParameter(method, 0); assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode()); assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode()); diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java index 10f93f515031..f50d7a42eb4a 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ class ByteArrayDecoderTests extends AbstractDecoderTests { @Override @Test - public void canDecode() { + protected void canDecode() { assertThat(this.decoder.canDecode(ResolvableType.forClass(byte[].class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), @@ -56,7 +56,7 @@ public void canDecode() { @Override @Test - public void decode() { + protected void decode() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); @@ -70,7 +70,7 @@ public void decode() { @Override @Test - public void decodeToMono() { + protected void decodeToMono() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java index dcb5043f2974..7e2eadc3b6eb 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class ByteArrayEncoderTests extends AbstractEncoderTests { @Override @Test - public void canEncode() { + protected void canEncode() { assertThat(this.encoder.canEncode(ResolvableType.forClass(byte[].class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), @@ -57,7 +57,7 @@ public void canEncode() { @Override @Test - public void encode() { + protected void encode() { Flux input = Flux.just(this.fooBytes, this.barBytes); testEncodeAll(input, byte[].class, step -> step diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java index d14a7543609a..65c5c57dd510 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ class ByteBufferDecoderTests extends AbstractDecoderTests { @Override @Test - public void canDecode() { + protected void canDecode() { assertThat(this.decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), @@ -57,7 +57,7 @@ public void canDecode() { @Override @Test - public void decode() { + protected void decode() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); @@ -72,7 +72,7 @@ public void decode() { @Override @Test - public void decodeToMono() { + protected void decodeToMono() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java index 1bc0e3f665d6..50b93c9f3f6e 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class ByteBufferEncoderTests extends AbstractEncoderTests { @Override @Test - public void canEncode() { + protected void canEncode() { assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), @@ -57,7 +57,7 @@ public void canEncode() { @Override @Test - public void encode() { + protected void encode() { Flux input = Flux.just(this.fooBytes, this.barBytes) .map(ByteBuffer::wrap); diff --git a/spring-core/src/test/java/org/springframework/core/codec/CharBufferDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/CharBufferDecoderTests.java new file mode 100644 index 000000000000..7f0c46be8145 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/CharBufferDecoderTests.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.codec; + +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +import static java.nio.charset.StandardCharsets.UTF_16BE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CharBufferDecoder}. + * + * @author Markus Heiden + * @author Arjen Poutsma + */ +class CharBufferDecoderTests extends AbstractDecoderTests { + + private static final ResolvableType TYPE = ResolvableType.forClass(CharBuffer.class); + + CharBufferDecoderTests() { + super(CharBufferDecoder.allMimeTypes()); + } + + @Override + @Test + protected void canDecode() { + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.TEXT_HTML)).isTrue(); + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.APPLICATION_JSON)).isTrue(); + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.parseMimeType("text/plain;charset=utf-8"))).isTrue(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Object.class), MimeTypeUtils.APPLICATION_JSON)).isFalse(); + } + + @Override + @Test + protected void decode() { + CharBuffer u = charBuffer("ü"); + CharBuffer e = charBuffer("é"); + CharBuffer o = charBuffer("ø"); + String s = String.format("%s\n%s\n%s", u, e, o); + Flux input = toDataBuffers(s, 1, UTF_8); + + testDecodeAll(input, TYPE, step -> step.expectNext(u, e, o).verifyComplete(), null, null); + } + + @Test + void decodeMultibyteCharacterUtf16() { + CharBuffer u = charBuffer("ü"); + CharBuffer e = charBuffer("é"); + CharBuffer o = charBuffer("ø"); + String s = String.format("%s\n%s\n%s", u, e, o); + Flux source = toDataBuffers(s, 2, UTF_16BE); + MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain;charset=utf-16be"); + + testDecode(source, TYPE, step -> step.expectNext(u, e, o).verifyComplete(), mimeType, null); + } + + private Flux toDataBuffers(String s, int length, Charset charset) { + byte[] bytes = s.getBytes(charset); + List chunks = new ArrayList<>(); + for (int i = 0; i < bytes.length; i += length) { + chunks.add(Arrays.copyOfRange(bytes, i, i + length)); + } + return Flux.fromIterable(chunks) + .map(chunk -> { + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(length); + dataBuffer.write(chunk, 0, chunk.length); + return dataBuffer; + }); + } + + @Test + void decodeNewLine() { + Flux input = Flux.just( + stringBuffer("\r\nabc\n"), + stringBuffer("def"), + stringBuffer("ghi\r\n\n"), + stringBuffer("jkl"), + stringBuffer("mno\npqr\n"), + stringBuffer("stu"), + stringBuffer("vw"), + stringBuffer("xyz") + ); + + testDecode(input, CharBuffer.class, step -> step + .expectNext(charBuffer("")).as("1st") + .expectNext(charBuffer("abc")) + .expectNext(charBuffer("defghi")) + .expectNext(charBuffer("")).as("2nd") + .expectNext(charBuffer("jklmno")) + .expectNext(charBuffer("pqr")) + .expectNext(charBuffer("stuvwxyz")) + .expectComplete() + .verify()); + } + + @Test + void decodeNewlinesAcrossBuffers() { + Flux input = Flux.just( + stringBuffer("\r"), + stringBuffer("\n"), + stringBuffer("xyz") + ); + + testDecode(input, CharBuffer.class, step -> step + .expectNext(charBuffer("")) + .expectNext(charBuffer("xyz")) + .expectComplete() + .verify()); + } + + @Test + void maxInMemoryLimit() { + Flux input = Flux.just( + stringBuffer("abc\n"), stringBuffer("defg\n"), + stringBuffer("hi"), stringBuffer("jkl"), stringBuffer("mnop")); + + this.decoder.setMaxInMemorySize(5); + + testDecode(input, CharBuffer.class, step -> step + .expectNext(charBuffer("abc")) + .expectNext(charBuffer("defg")) + .verifyError(DataBufferLimitException.class)); + } + + @Test + void maxInMemoryLimitDoesNotApplyToParsedItemsThatDontRequireBuffering() { + Flux input = Flux.just( + stringBuffer("TOO MUCH DATA\nanother line\n\nand another\n")); + + this.decoder.setMaxInMemorySize(5); + + testDecode(input, CharBuffer.class, step -> step + .expectNext(charBuffer("TOO MUCH DATA")) + .expectNext(charBuffer("another line")) + .expectNext(charBuffer("")) + .expectNext(charBuffer("and another")) + .expectComplete() + .verify()); + } + + @Test + // gh-24339 + void maxInMemoryLimitReleaseUnprocessedLinesWhenUnlimited() { + Flux input = Flux.just(stringBuffer("Line 1\nLine 2\nLine 3\n")); + + this.decoder.setMaxInMemorySize(-1); + testDecodeCancel(input, ResolvableType.forClass(String.class), null, Collections.emptyMap()); + } + + @Test + void decodeNewLineIncludeDelimiters() { + this.decoder = CharBufferDecoder.allMimeTypes(CharBufferDecoder.DEFAULT_DELIMITERS, false); + + Flux input = Flux.just( + stringBuffer("\r\nabc\n"), + stringBuffer("def"), + stringBuffer("ghi\r\n\n"), + stringBuffer("jkl"), + stringBuffer("mno\npqr\n"), + stringBuffer("stu"), + stringBuffer("vw"), + stringBuffer("xyz") + ); + + testDecode(input, CharBuffer.class, step -> step + .expectNext(charBuffer("\r\n")) + .expectNext(charBuffer("abc\n")) + .expectNext(charBuffer("defghi\r\n")) + .expectNext(charBuffer("\n")) + .expectNext(charBuffer("jklmno\n")) + .expectNext(charBuffer("pqr\n")) + .expectNext(charBuffer("stuvwxyz")) + .expectComplete() + .verify()); + } + + @Test + void decodeEmptyFlux() { + Flux input = Flux.empty(); + + testDecode(input, String.class, step -> step + .expectComplete() + .verify()); + } + + @Test + void decodeEmptyDataBuffer() { + Flux input = Flux.just(stringBuffer("")); + Flux output = this.decoder.decode(input, + TYPE, null, Collections.emptyMap()); + + StepVerifier.create(output) + .expectNext(charBuffer("")) + .expectComplete().verify(); + } + + @Override + @Test + protected void decodeToMono() { + Flux input = Flux.just( + stringBuffer("foo"), + stringBuffer("bar"), + stringBuffer("baz")); + + testDecodeToMonoAll(input, CharBuffer.class, step -> step + .expectNext(charBuffer("foobarbaz")) + .expectComplete() + .verify()); + } + + @Test + void decodeToMonoWithEmptyFlux() { + Flux input = Flux.empty(); + + testDecodeToMono(input, String.class, step -> step + .expectComplete() + .verify()); + } + + private DataBuffer stringBuffer(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + } + + private CharBuffer charBuffer(String value) { + return CharBuffer + .allocate(value.length()) + .put(value) + .flip(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java index 0aa583ae978d..aea1f05befe8 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ class CharSequenceEncoderTests extends AbstractEncoderTests @Override @Test - public void canEncode() throws Exception { + protected void canEncode() { assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.encoder.canEncode(ResolvableType.forClass(StringBuilder.class), @@ -66,7 +66,7 @@ public void canEncode() throws Exception { @Override @Test - public void encode() { + protected void encode() { Flux input = Flux.just(this.foo, this.bar); testEncodeAll(input, CharSequence.class, step -> step diff --git a/spring-core/src/test/java/org/springframework/core/codec/DataBufferDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/DataBufferDecoderTests.java index e1296537bfd3..822bb200a9c5 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/DataBufferDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/DataBufferDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ class DataBufferDecoderTests extends AbstractDecoderTests { @Override @Test - public void canDecode() { + protected void canDecode() { assertThat(this.decoder.canDecode(ResolvableType.forClass(DataBuffer.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), @@ -57,7 +57,7 @@ public void canDecode() { @Override @Test - public void decode() { + protected void decode() { Flux input = Flux.just( this.bufferFactory.wrap(this.fooBytes), this.bufferFactory.wrap(this.barBytes)); @@ -70,7 +70,7 @@ public void decode() { @Override @Test - public void decodeToMono() throws Exception { + protected void decodeToMono() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); diff --git a/spring-core/src/test/java/org/springframework/core/codec/DataBufferEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/DataBufferEncoderTests.java index 9a578ed403ff..adde58f453ab 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/DataBufferEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/DataBufferEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ class DataBufferEncoderTests extends AbstractEncoderTests { @Override @Test - public void canEncode() { + protected void canEncode() { assertThat(this.encoder.canEncode(ResolvableType.forClass(DataBuffer.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), @@ -59,7 +59,7 @@ public void canEncode() { @Override @Test - public void encode() throws Exception { + protected void encode() { Flux input = Flux.just(this.fooBytes, this.barBytes) .flatMap(bytes -> Mono.defer(() -> { DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(bytes.length); diff --git a/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferDecoderTests.java index 2260d74ecfd9..a1220d099cc4 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ class Netty5BufferDecoderTests extends AbstractDecoderTests @Override @Test - public void canDecode() { + protected void canDecode() { assertThat(this.decoder.canDecode(ResolvableType.forClass(Buffer.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), @@ -58,7 +58,7 @@ public void canDecode() { @Override @Test - public void decode() { + protected void decode() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); @@ -71,7 +71,7 @@ public void decode() { @Override @Test - public void decodeToMono() { + protected void decodeToMono() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); diff --git a/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufDecoderTests.java index 7249fdcb750e..83be79db6150 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ class NettyByteBufDecoderTests extends AbstractDecoderTests @Override @Test - public void canDecode() { + protected void canDecode() { assertThat(this.decoder.canDecode(ResolvableType.forClass(ByteBuf.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), @@ -58,7 +58,7 @@ public void canDecode() { @Override @Test - public void decode() { + protected void decode() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); @@ -71,7 +71,7 @@ public void decode() { @Override @Test - public void decodeToMono() { + protected void decodeToMono() { Flux input = Flux.concat( dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); diff --git a/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufEncoderTests.java index 0c6bff3792fe..70bbd32a46fe 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ class NettyByteBufEncoderTests extends AbstractEncoderTests @Override @Test - public void canEncode() { + protected void canEncode() { assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteBuf.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), @@ -58,7 +58,7 @@ public void canEncode() { @Override @Test - public void encode() { + protected void encode() { Flux input = Flux.just(this.fooBytes, this.barBytes).map(Unpooled::copiedBuffer); testEncodeAll(input, ByteBuf.class, step -> step diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java index f060ce4f3598..d66e0d8651b4 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ class ResourceDecoderTests extends AbstractDecoderTests { @Override @Test - public void canDecode() { + protected void canDecode() { assertThat(this.decoder.canDecode(forClass(InputStreamResource.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(forClass(ByteArrayResource.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(forClass(Resource.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); @@ -62,7 +62,7 @@ public void canDecode() { @Override @Test - public void decode() { + protected void decode() { Flux input = Flux.concat(dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); testDecodeAll(input, Resource.class, step -> step @@ -81,7 +81,7 @@ public void decode() { @Override @Test - public void decodeToMono() { + protected void decodeToMono() { Flux input = Flux.concat(dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); testDecodeToMonoAll(input, ResolvableType.forClass(Resource.class), step -> step @@ -103,7 +103,7 @@ public void decodeToMono() { } @Test - public void decodeInputStreamResource() { + void decodeInputStreamResource() { Flux input = Flux.concat(dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); testDecodeAll(input, InputStreamResource.class, step -> step .consumeNextWith(resource -> { diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java index 1925415e7345..957ec66214c9 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ class ResourceEncoderTests extends AbstractEncoderTests { @Override @Test - public void canEncode() { + protected void canEncode() { assertThat(this.encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteArrayResource.class), @@ -66,7 +66,7 @@ public void canEncode() { @Override @Test - public void encode() { + protected void encode() { Flux input = Flux.just(new ByteArrayResource(this.bytes)); testEncodeAll(input, Resource.class, step -> step diff --git a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java index df83b60f4247..e44c2fcb44d0 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link StringDecoder}. + * Tests for {@link StringDecoder}. * * @author Sebastien Deleuze * @author Brian Clozel @@ -58,7 +58,7 @@ class StringDecoderTests extends AbstractDecoderTests { @Override @Test - public void canDecode() { + protected void canDecode() { assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.TEXT_PLAIN)).isTrue(); assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.TEXT_HTML)).isTrue(); assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.APPLICATION_JSON)).isTrue(); @@ -69,7 +69,7 @@ public void canDecode() { @Override @Test - public void decode() { + protected void decode() { String u = "ü"; String e = "é"; String o = "ø"; @@ -139,7 +139,7 @@ void decodeNewLine() { } @Test - void decodeNewlinesAcrossBuffers() { + void decodeNewlinesAcrossBuffers() { Flux input = Flux.just( stringBuffer("\r"), stringBuffer("\n"), @@ -238,7 +238,7 @@ void decodeEmptyDataBuffer() { @Override @Test - public void decodeToMono() { + protected void decodeToMono() { Flux input = Flux.just( stringBuffer("foo"), stringBuffer("bar"), diff --git a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java index 5fe86254899b..ac097a382f5d 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,11 +34,13 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -301,7 +303,7 @@ void fieldListOfListUnknown() throws Exception { void fieldArray() throws Exception { TypeDescriptor typeDescriptor = new TypeDescriptor(TypeDescriptorTests.class.getDeclaredField("intArray")); assertThat(typeDescriptor.isArray()).isTrue(); - assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(Integer.TYPE); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(int.class); assertThat(typeDescriptor.toString()).isEqualTo("int[]"); } @@ -357,21 +359,21 @@ void valueOfPrimitive() { assertThat(typeDescriptor.isArray()).isFalse(); assertThat(typeDescriptor.isCollection()).isFalse(); assertThat(typeDescriptor.isMap()).isFalse(); - assertThat(typeDescriptor.getType()).isEqualTo(Integer.TYPE); + assertThat(typeDescriptor.getType()).isEqualTo(int.class); assertThat(typeDescriptor.getObjectType()).isEqualTo(Integer.class); } @Test - void valueOfArray() throws Exception { + void valueOfArray() { TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(int[].class); assertThat(typeDescriptor.isArray()).isTrue(); assertThat(typeDescriptor.isCollection()).isFalse(); assertThat(typeDescriptor.isMap()).isFalse(); - assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(Integer.TYPE); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(int.class); } @Test - void valueOfCollection() throws Exception { + void valueOfCollection() { TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(Collection.class); assertThat(typeDescriptor.isCollection()).isTrue(); assertThat(typeDescriptor.isArray()).isFalse(); @@ -410,7 +412,7 @@ void nestedMethodParameterTypeMapTwoLevels() throws Exception { } @Test - void nestedMethodParameterNot1NestedLevel() throws Exception { + void nestedMethodParameterNot1NestedLevel() { assertThatIllegalArgumentException().isThrownBy(() -> TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test4", List.class), 0, 2), 2)); } @@ -428,7 +430,7 @@ void nestedMethodParameterTypeNotNestable() throws Exception { } @Test - void nestedMethodParameterTypeInvalidNestingLevel() throws Exception { + void nestedMethodParameterTypeInvalidNestingLevel() { assertThatIllegalArgumentException().isThrownBy(() -> TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test5", String.class), 0, 2), 2)); } @@ -663,12 +665,12 @@ void passDownGeneric() throws Exception { } @Test - void upCast() throws Exception { + void upcast() throws Exception { Property property = new Property(getClass(), getClass().getMethod("getProperty"), getClass().getMethod("setProperty", Map.class)); TypeDescriptor typeDescriptor = new TypeDescriptor(property); - TypeDescriptor upCast = typeDescriptor.upcast(Object.class); - assertThat(upCast.getAnnotation(MethodAnnotation1.class)).isNotNull(); + TypeDescriptor upcast = typeDescriptor.upcast(Object.class); + assertThat(upcast.getAnnotation(MethodAnnotation1.class)).isNotNull(); } @Test @@ -682,7 +684,7 @@ void upCastNotSuper() throws Exception { } @Test - void elementTypeForCollectionSubclass() throws Exception { + void elementTypeForCollectionSubclass() { @SuppressWarnings("serial") class CustomSet extends HashSet { } @@ -692,7 +694,7 @@ class CustomSet extends HashSet { } @Test - void elementTypeForMapSubclass() throws Exception { + void elementTypeForMapSubclass() { @SuppressWarnings("serial") class CustomMap extends HashMap { } @@ -704,7 +706,7 @@ class CustomMap extends HashMap { } @Test - void createMapArray() throws Exception { + void createMapArray() { TypeDescriptor mapType = TypeDescriptor.map( LinkedHashMap.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)); TypeDescriptor arrayType = TypeDescriptor.array(mapType); @@ -713,13 +715,13 @@ void createMapArray() throws Exception { } @Test - void createStringArray() throws Exception { + void createStringArray() { TypeDescriptor arrayType = TypeDescriptor.array(TypeDescriptor.valueOf(String.class)); assertThat(TypeDescriptor.valueOf(String[].class)).isEqualTo(arrayType); } @Test - void createNullArray() throws Exception { + void createNullArray() { assertThat((Object) TypeDescriptor.array(null)).isNull(); } @@ -736,13 +738,13 @@ void serializable() throws Exception { } @Test - void createCollectionWithNullElement() throws Exception { + void createCollectionWithNullElement() { TypeDescriptor typeDescriptor = TypeDescriptor.collection(List.class, null); assertThat(typeDescriptor.getElementTypeDescriptor()).isNull(); } @Test - void createMapWithNullElements() throws Exception { + void createMapWithNullElements() { TypeDescriptor typeDescriptor = TypeDescriptor.map(LinkedHashMap.class, null, null); assertThat(typeDescriptor.getMapKeyTypeDescriptor()).isNull(); assertThat(typeDescriptor.getMapValueTypeDescriptor()).isNull(); @@ -757,6 +759,17 @@ void getSource() throws Exception { assertThat(TypeDescriptor.valueOf(Integer.class).getSource()).isEqualTo(Integer.class); } + @Test // gh-31672 + void equalityWithGenerics() { + ResolvableType rt1 = ResolvableType.forClassWithGenerics(Optional.class, Integer.class); + ResolvableType rt2 = ResolvableType.forClassWithGenerics(Optional.class, String.class); + + TypeDescriptor td1 = new TypeDescriptor(rt1, null, null); + TypeDescriptor td2 = new TypeDescriptor(rt2, null, null); + + assertThat(td1).isNotEqualTo(td2); + } + // Methods designed for test introspection diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/ConvertingComparatorTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/ConvertingComparatorTests.java index b7903d069752..1bebfc3d10e2 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/ConvertingComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/ConvertingComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.util.comparator.ComparableComparator; +import org.springframework.util.comparator.Comparators; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -45,45 +45,45 @@ class ConvertingComparatorTests { private final TestComparator comparator = new TestComparator(); @Test - void shouldThrowOnNullComparator() throws Exception { + void shouldThrowOnNullComparator() { assertThatIllegalArgumentException().isThrownBy(() -> new ConvertingComparator<>(null, this.converter)); } @Test - void shouldThrowOnNullConverter() throws Exception { + void shouldThrowOnNullConverter() { assertThatIllegalArgumentException().isThrownBy(() -> new ConvertingComparator(this.comparator, null)); } @Test - void shouldThrowOnNullConversionService() throws Exception { + void shouldThrowOnNullConversionService() { assertThatIllegalArgumentException().isThrownBy(() -> new ConvertingComparator(this.comparator, null, Integer.class)); } @Test - void shouldThrowOnNullType() throws Exception { + void shouldThrowOnNullType() { assertThatIllegalArgumentException().isThrownBy(() -> new ConvertingComparator(this.comparator, this.conversionService, null)); } @Test - void shouldUseConverterOnCompare() throws Exception { + void shouldUseConverterOnCompare() { ConvertingComparator convertingComparator = new ConvertingComparator<>( this.comparator, this.converter); testConversion(convertingComparator); } @Test - void shouldUseConversionServiceOnCompare() throws Exception { + void shouldUseConversionServiceOnCompare() { ConvertingComparator convertingComparator = new ConvertingComparator<>( comparator, conversionService, Integer.class); testConversion(convertingComparator); } @Test - void shouldGetForConverter() throws Exception { + void shouldGetForConverter() { testConversion(new ConvertingComparator<>(comparator, converter)); } @@ -95,17 +95,17 @@ private void testConversion(ConvertingComparator convertingComp } @Test - void shouldGetMapEntryKeys() throws Exception { + void shouldGetMapEntryKeys() { ArrayList> list = createReverseOrderMapEntryList(); - Comparator> comparator = ConvertingComparator.mapEntryKeys(new ComparableComparator()); + Comparator> comparator = ConvertingComparator.mapEntryKeys(Comparators.comparable()); list.sort(comparator); assertThat(list.get(0).getKey()).isEqualTo("a"); } @Test - void shouldGetMapEntryValues() throws Exception { + void shouldGetMapEntryValues() { ArrayList> list = createReverseOrderMapEntryList(); - Comparator> comparator = ConvertingComparator.mapEntryValues(new ComparableComparator()); + Comparator> comparator = ConvertingComparator.mapEntryValues(Comparators.comparable()); list.sort(comparator); assertThat(list.get(0).getValue()).isEqualTo(1); } @@ -130,7 +130,7 @@ public Integer convert(String source) { } - private static class TestComparator extends ComparableComparator { + private static class TestComparator implements Comparator { private boolean called; @@ -139,7 +139,7 @@ public int compare(Integer o1, Integer o2) { assertThat(o1).isInstanceOf(Integer.class); assertThat(o2).isInstanceOf(Integer.class); this.called = true; - return super.compare(o1, o2); + return Comparators.comparable().compare(o1, o2); } public void assertCalled() { diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index 1709a9a2032d..546e09145937 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import java.util.Set; import java.util.TimeZone; import java.util.UUID; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -57,7 +58,7 @@ import static org.assertj.core.api.Assertions.entry; /** - * Unit tests for {@link DefaultConversionService}. + * Tests for {@link DefaultConversionService}. * *

      In this package for enforcing accessibility checks to non-public classes outside * the {@code org.springframework.core.convert.support} implementation package. @@ -318,6 +319,24 @@ void uuidToStringAndStringToUuid() { assertThat(convertToUUID).isEqualTo(uuid); } + @Test + void stringToPatternEmptyString() { + assertThat(conversionService.convert("", Pattern.class)).isNull(); + } + + @Test + void stringToPattern() { + String pattern = "\\s"; + assertThat(conversionService.convert(pattern, Pattern.class)) + .isInstanceOfSatisfying(Pattern.class, regex -> assertThat(regex.pattern()).isEqualTo(pattern)); + } + + @Test + void patternToString() { + String regex = "\\d"; + assertThat(conversionService.convert(Pattern.compile(regex), String.class)).isEqualTo(regex); + } + @Test void numberToNumber() { assertThat(conversionService.convert(1, Long.class)).isEqualTo(Long.valueOf(1)); @@ -345,7 +364,8 @@ void characterToNumber() { void convertArrayToCollectionInterface() { @SuppressWarnings("unchecked") Collection result = conversionService.convert(new String[] {"1", "2", "3"}, Collection.class); - assertThat(result).isExactlyInstanceOf(LinkedHashSet.class).containsExactly("1", "2", "3"); + assertThat(result).isEqualTo(List.of("1", "2", "3")); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly("1", "2", "3"); } @Test @@ -583,6 +603,12 @@ void convertStringArrayToIntArray() { assertThat(result).containsExactly(1, 2, 3); } + @Test + void convertIntArrayToStringArray() { + String[] result = conversionService.convert(new int[] {1, 2, 3}, String[].class); + assertThat(result).containsExactly("1", "2", "3"); + } + @Test void convertIntegerArrayToIntegerArray() { Integer[] result = conversionService.convert(new Integer[] {1, 2, 3}, Integer[].class); @@ -595,6 +621,12 @@ void convertIntegerArrayToIntArray() { assertThat(result).containsExactly(1, 2, 3); } + @Test + void convertIntArrayToIntegerArray() { + Integer[] result = conversionService.convert(new int[] {1, 2}, Integer[].class); + assertThat(result).containsExactly(1, 2); + } + @Test void convertObjectArrayToIntegerArray() { Integer[] result = conversionService.convert(new Object[] {1, 2, 3}, Integer[].class); @@ -607,15 +639,33 @@ void convertObjectArrayToIntArray() { assertThat(result).containsExactly(1, 2, 3); } + @Test // gh-33212 + void convertIntArrayToObjectArray() { + Object[] result = conversionService.convert(new int[] {1, 2}, Object[].class); + assertThat(result).containsExactly(1, 2); + } + + @Test + void convertIntArrayToFloatArray() { + Float[] result = conversionService.convert(new int[] {1, 2}, Float[].class); + assertThat(result).containsExactly(1.0F, 2.0F); + } + + @Test + void convertIntArrayToPrimitiveFloatArray() { + float[] result = conversionService.convert(new int[] {1, 2}, float[].class); + assertThat(result).containsExactly(1.0F, 2.0F); + } + @Test - void convertByteArrayToWrapperArray() { + void convertPrimitiveByteArrayToByteWrapperArray() { byte[] byteArray = {1, 2, 3}; Byte[] converted = conversionService.convert(byteArray, Byte[].class); assertThat(converted).isEqualTo(new Byte[]{1, 2, 3}); } @Test - void convertArrayToArrayAssignable() { + void convertIntArrayToIntArray() { int[] result = conversionService.convert(new int[] {1, 2, 3}, int[].class); assertThat(result).containsExactly(1, 2, 3); } @@ -842,7 +892,7 @@ void convertObjectToObjectFinderMethod() { void convertObjectToObjectFinderMethodWithNull() { TestEntity entity = (TestEntity) conversionService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(TestEntity.class)); - assertThat((Object) entity).isNull(); + assertThat(entity).isNull(); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java index 6d0f1751d900..cb2f7f82858d 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.core.convert.support; import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URL; @@ -96,11 +95,11 @@ void emptyListToListDifferentTargetType() throws Exception { @SuppressWarnings("unchecked") ArrayList result = (ArrayList) conversionService.convert(list, sourceType, targetType); assertThat(result.getClass()).isEqualTo(ArrayList.class); - assertThat(result.isEmpty()).isTrue(); + assertThat(result).isEmpty(); } @Test - void collectionToObjectInteraction() throws Exception { + void collectionToObjectInteraction() { List> list = new ArrayList<>(); list.add(Arrays.asList("9", "12")); list.add(Arrays.asList("37", "23")); @@ -111,7 +110,7 @@ void collectionToObjectInteraction() throws Exception { @Test @SuppressWarnings("unchecked") - void arrayCollectionToObjectInteraction() throws Exception { + void arrayCollectionToObjectInteraction() { List[] array = new List[2]; array[0] = Arrays.asList("9", "12"); array[1] = Arrays.asList("37", "23"); @@ -134,18 +133,19 @@ void objectToCollection() throws Exception { TypeDescriptor targetType = new TypeDescriptor(getClass().getField("objectToCollection")); assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); List>> result = (List>>) conversionService.convert(list, sourceType, targetType); - assertThat(result.get(0).get(0).get(0)).isEqualTo((Integer) 9); - assertThat(result.get(0).get(1).get(0)).isEqualTo((Integer) 12); - assertThat(result.get(1).get(0).get(0)).isEqualTo((Integer) 37); - assertThat(result.get(1).get(1).get(0)).isEqualTo((Integer) 23); + assertThat(result).hasSize(2); + assertThat(result.get(0).get(0)).singleElement().isEqualTo(9); + assertThat(result.get(0).get(1)).singleElement().isEqualTo(12); + assertThat(result.get(1).get(0)).singleElement().isEqualTo(37); + assertThat(result.get(1).get(1)).singleElement().isEqualTo(23); } @Test @SuppressWarnings("unchecked") void stringToCollection() throws Exception { List> list = new ArrayList<>(); - list.add(Arrays.asList("9,12")); - list.add(Arrays.asList("37,23")); + list.add(List.of("9,12")); + list.add(List.of("37,23")); conversionService.addConverterFactory(new StringToNumberConverterFactory()); conversionService.addConverter(new StringToCollectionConverter(conversionService)); conversionService.addConverter(new ObjectToCollectionConverter(conversionService)); @@ -154,10 +154,9 @@ void stringToCollection() throws Exception { TypeDescriptor targetType = new TypeDescriptor(getClass().getField("objectToCollection")); assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); List>> result = (List>>) conversionService.convert(list, sourceType, targetType); - assertThat(result.get(0).get(0).get(0)).isEqualTo((Integer) 9); - assertThat(result.get(0).get(0).get(1)).isEqualTo((Integer) 12); - assertThat(result.get(1).get(0).get(0)).isEqualTo((Integer) 37); - assertThat(result.get(1).get(0).get(1)).isEqualTo((Integer) 23); + assertThat(result).satisfiesExactly( + zero -> assertThat(zero.get(0)).containsExactly(9, 12), + one -> assertThat(one.get(0)).containsExactly(37, 23)); } @Test @@ -238,7 +237,7 @@ void elementTypesNotConvertible() throws Exception { } @Test - void nothingInCommon() throws Exception { + void nothingInCommon() { List resources = new ArrayList<>(); resources.add(new ClassPathResource("test")); resources.add(3); @@ -276,10 +275,10 @@ void stringToEnumSet() throws Exception { public EnumSet enumSet; - public static abstract class BaseResource implements Resource { + public abstract static class BaseResource implements Resource { @Override - public InputStream getInputStream() throws IOException { + public InputStream getInputStream() { return null; } @@ -304,32 +303,32 @@ public boolean isFile() { } @Override - public URL getURL() throws IOException { + public URL getURL() { return null; } @Override - public URI getURI() throws IOException { + public URI getURI() { return null; } @Override - public File getFile() throws IOException { + public File getFile() { return null; } @Override - public long contentLength() throws IOException { + public long contentLength() { return 0; } @Override - public long lastModified() throws IOException { + public long lastModified() { return 0; } @Override - public Resource createRelative(String relativePath) throws IOException { + public Resource createRelative(String relativePath) { return null; } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index 99b6ae3f6f87..bec17c058be6 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for {@link GenericConversionService}. + * Tests for {@link GenericConversionService}. * *

      In this package for access to package-local converter implementations. * @@ -386,7 +386,7 @@ void convertiblePairEqualsAndHash() { void convertiblePairDifferentEqualsAndHash() { GenericConverter.ConvertiblePair pair = new GenericConverter.ConvertiblePair(Number.class, String.class); GenericConverter.ConvertiblePair pairOpposite = new GenericConverter.ConvertiblePair(String.class, Number.class); - assertThat(pair.equals(pairOpposite)).isFalse(); + assertThat(pair).isNotEqualTo(pairOpposite); assertThat(pair.hashCode()).isNotEqualTo(pairOpposite.hashCode()); } @@ -476,7 +476,7 @@ void enumToStringConversion() { } @Test - void subclassOfEnumToString() throws Exception { + void subclassOfEnumToString() { conversionService.addConverter(new EnumToStringConverter(conversionService)); assertThat(conversionService.convert(EnumWithSubclass.FIRST, String.class)).isEqualTo("FIRST"); } @@ -633,7 +633,7 @@ public Integer[] convert(String[] source) { } - private static class MyStringToIntegerArrayConverter implements Converter { + private static class MyStringToIntegerArrayConverter implements Converter { @Override public Integer[] convert(String source) { diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java index 09428a6c3e5d..cd4e748122e9 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,13 +72,13 @@ void scalarMap() throws Exception { assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); @SuppressWarnings("unchecked") Map result = (Map) conversionService.convert(map, sourceType, targetType); - assertThat(map.equals(result)).isFalse(); + assertThat(map).isNotEqualTo(result); assertThat((int) result.get(1)).isEqualTo(9); assertThat((int) result.get(2)).isEqualTo(37); } @Test - void scalarMapNotGenericTarget() throws Exception { + void scalarMapNotGenericTarget() { Map map = new HashMap<>(); map.put("1", "9"); map.put("2", "37"); @@ -107,7 +107,7 @@ void scalarMapNotGenericSourceField() throws Exception { assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); @SuppressWarnings("unchecked") Map result = (Map) conversionService.convert(map, sourceType, targetType); - assertThat(map.equals(result)).isFalse(); + assertThat(map).isNotEqualTo(result); assertThat((int) result.get(1)).isEqualTo(9); assertThat((int) result.get(2)).isEqualTo(37); } @@ -133,7 +133,7 @@ void collectionMap() throws Exception { assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); @SuppressWarnings("unchecked") Map> result = (Map>) conversionService.convert(map, sourceType, targetType); - assertThat(map.equals(result)).isFalse(); + assertThat(map).isNotEqualTo(result); assertThat(result.get(1)).isEqualTo(Arrays.asList(9, 12)); assertThat(result.get(2)).isEqualTo(Arrays.asList(37, 23)); } @@ -155,13 +155,13 @@ void collectionMapSourceTarget() throws Exception { assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); @SuppressWarnings("unchecked") Map> result = (Map>) conversionService.convert(map, sourceType, targetType); - assertThat(map.equals(result)).isFalse(); + assertThat(map).isNotEqualTo(result); assertThat(result.get(1)).isEqualTo(Arrays.asList(9, 12)); assertThat(result.get(2)).isEqualTo(Arrays.asList(37, 23)); } @Test - void collectionMapNotGenericTarget() throws Exception { + void collectionMapNotGenericTarget() { Map> map = new HashMap<>(); map.put("1", Arrays.asList("9", "12")); map.put("2", Arrays.asList("37", "23")); @@ -171,7 +171,7 @@ void collectionMapNotGenericTarget() throws Exception { } @Test - void collectionMapNotGenericTargetCollectionToObjectInteraction() throws Exception { + void collectionMapNotGenericTargetCollectionToObjectInteraction() { Map> map = new HashMap<>(); map.put("1", Arrays.asList("9", "12")); map.put("2", Arrays.asList("37", "23")); @@ -193,7 +193,7 @@ void emptyMap() throws Exception { } @Test - void emptyMapNoTargetGenericInfo() throws Exception { + void emptyMapNoTargetGenericInfo() { Map map = new HashMap<>(); assertThat(conversionService.canConvert(Map.class, Map.class)).isTrue(); @@ -214,7 +214,7 @@ void emptyMapDifferentTargetImplType() throws Exception { } @Test - void noDefaultConstructorCopyNotRequired() throws Exception { + void noDefaultConstructorCopyNotRequired() { // SPR-9284 NoDefaultConstructorMap map = new NoDefaultConstructorMap<>( Collections.singletonMap("1", 1)); @@ -256,8 +256,8 @@ void mapToMultiValueMap() throws Exception { MultiValueMap converted = (MultiValueMap) conversionService.convert(source, targetType); assertThat(converted).hasSize(2); - assertThat(converted.get("a")).isEqualTo(Arrays.asList("1")); - assertThat(converted.get("b")).isEqualTo(Arrays.asList("2")); + assertThat(converted.get("a")).isEqualTo(List.of("1")); + assertThat(converted.get("b")).isEqualTo(List.of("2")); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java index 4af44b77f4fa..2534ad9d4ae6 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Unit tests for {@link ObjectToObjectConverter}. + * Tests for {@link ObjectToObjectConverter}. * * @author Sam Brannen * @author Phillip Webb diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/StreamConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/StreamConverterTests.java index 56ec69a20612..cf5ae5468e51 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/StreamConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/StreamConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,7 +98,6 @@ void convertFromStreamToArrayNoConverter() throws NoSuchFieldException { } @Test - @SuppressWarnings("resource") void convertFromListToStream() throws NoSuchFieldException { this.conversionService.addConverterFactory(new StringToNumberConverterFactory()); List list = Arrays.asList("1", "2", "3"); @@ -112,7 +111,6 @@ void convertFromListToStream() throws NoSuchFieldException { } @Test - @SuppressWarnings("resource") void convertFromArrayToStream() throws NoSuchFieldException { Integer[] array = new Integer[] {1, 0, 1}; this.conversionService.addConverter(Integer.class, Boolean.class, source -> source == 1); @@ -123,7 +121,6 @@ void convertFromArrayToStream() throws NoSuchFieldException { } @Test - @SuppressWarnings("resource") void convertFromListToRawStream() throws NoSuchFieldException { List list = Arrays.asList("1", "2", "3"); TypeDescriptor streamOfInteger = new TypeDescriptor(Types.class.getField("rawStream")); diff --git a/spring-core/src/test/java/org/springframework/core/env/CompositePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/CompositePropertySourceTests.java index d7bafe37643a..773e9bcfdb28 100644 --- a/spring-core/src/test/java/org/springframework/core/env/CompositePropertySourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/CompositePropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.core.env; -import java.util.Collections; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -26,24 +26,32 @@ * Tests for {@link CompositePropertySource}. * * @author Phillip Webb + * @author Sam Brannen */ class CompositePropertySourceTests { @Test void addFirst() { - PropertySource p1 = new MapPropertySource("p1", Collections.emptyMap()); - PropertySource p2 = new MapPropertySource("p2", Collections.emptyMap()); - PropertySource p3 = new MapPropertySource("p3", Collections.emptyMap()); + PropertySource p1 = new MapPropertySource("p1", Map.of()); + PropertySource p2 = new MapPropertySource("p2", Map.of()); + PropertySource p3 = new MapPropertySource("p3", Map.of()); CompositePropertySource composite = new CompositePropertySource("c"); composite.addPropertySource(p2); composite.addPropertySource(p3); composite.addPropertySource(p1); composite.addFirstPropertySource(p1); - String s = composite.toString(); - int i1 = s.indexOf("name='p1'"); - int i2 = s.indexOf("name='p2'"); - int i3 = s.indexOf("name='p3'"); - assertThat(((i1 < i2) && (i2 < i3))).as("Bad order: " + s).isTrue(); + + assertThat(composite.getPropertySources()).extracting(PropertySource::getName).containsExactly("p1", "p2", "p3"); + assertThat(composite).asString().containsSubsequence("name='p1'", "name='p2'", "name='p3'"); + } + + @Test + void getPropertyNamesRemovesDuplicates() { + CompositePropertySource composite = new CompositePropertySource("c"); + composite.addPropertySource(new MapPropertySource("p1", Map.of("p1.property", "value"))); + composite.addPropertySource(new MapPropertySource("p2", + Map.of("p2.property1", "value", "p1.property", "value", "p2.property2", "value"))); + assertThat(composite.getPropertyNames()).containsOnly("p1.property", "p2.property1", "p2.property2"); } } diff --git a/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java index 2c87ad806d98..c408bce244a6 100644 --- a/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.core.env; -import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -32,14 +30,18 @@ * Unit tests covering the extensibility of {@link AbstractEnvironment}. * * @author Chris Beams + * @author Sam Brannen * @since 3.1 */ class CustomEnvironmentTests { + private static final String DEFAULT_PROFILE = AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME; + + @Test void control() { Environment env = new AbstractEnvironment() { }; - assertThat(env.acceptsProfiles(defaultProfile())).isTrue(); + assertThat(env.matchesProfiles(DEFAULT_PROFILE)).isTrue(); } @Test @@ -47,12 +49,12 @@ void withNoReservedDefaultProfile() { class CustomEnvironment extends AbstractEnvironment { @Override protected Set getReservedDefaultProfiles() { - return Collections.emptySet(); + return Set.of(); } } Environment env = new CustomEnvironment(); - assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); + assertThat(env.matchesProfiles(DEFAULT_PROFILE)).isFalse(); } @Test @@ -60,51 +62,47 @@ void withSingleCustomReservedDefaultProfile() { class CustomEnvironment extends AbstractEnvironment { @Override protected Set getReservedDefaultProfiles() { - return Collections.singleton("rd1"); + return Set.of("rd1"); } } Environment env = new CustomEnvironment(); - assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); - assertThat(env.acceptsProfiles(Profiles.of("rd1"))).isTrue(); + assertThat(env.matchesProfiles(DEFAULT_PROFILE)).isFalse(); + assertThat(env.matchesProfiles("rd1")).isTrue(); } @Test void withMultiCustomReservedDefaultProfile() { class CustomEnvironment extends AbstractEnvironment { @Override - @SuppressWarnings("serial") protected Set getReservedDefaultProfiles() { - return new HashSet<>() {{ - add("rd1"); - add("rd2"); - }}; + return Set.of("rd1", "rd2"); } } ConfigurableEnvironment env = new CustomEnvironment(); - assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); - assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2"))).isTrue(); + assertThat(env.matchesProfiles(DEFAULT_PROFILE)).isFalse(); + assertThat(env.matchesProfiles("rd1 | rd2")).isTrue(); // finally, issue additional assertions to cover all combinations of calling these // methods, however unlikely. env.setDefaultProfiles("d1"); - assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2"))).isFalse(); - assertThat(env.acceptsProfiles(Profiles.of("d1"))).isTrue(); + assertThat(env.matchesProfiles("rd1 | rd2")).isFalse(); + assertThat(env.matchesProfiles("d1")).isTrue(); env.setActiveProfiles("a1", "a2"); - assertThat(env.acceptsProfiles(Profiles.of("d1"))).isFalse(); - assertThat(env.acceptsProfiles(Profiles.of("a1 | a2"))).isTrue(); + assertThat(env.matchesProfiles("d1")).isFalse(); + assertThat(env.matchesProfiles("a1 | a2")).isTrue(); env.setActiveProfiles(); - assertThat(env.acceptsProfiles(Profiles.of("d1"))).isTrue(); - assertThat(env.acceptsProfiles(Profiles.of("a1 | a2"))).isFalse(); + assertThat(env.matchesProfiles("d1")).isTrue(); + assertThat(env.matchesProfiles("a1 | a2")).isFalse(); env.setDefaultProfiles(); - assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); - assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2"))).isFalse(); - assertThat(env.acceptsProfiles(Profiles.of("d1"))).isFalse(); - assertThat(env.acceptsProfiles(Profiles.of("a1 | a2"))).isFalse(); + assertThat(env.matchesProfiles(DEFAULT_PROFILE)).isFalse(); + assertThat(env.matchesProfiles("rd1 | rd2")).isFalse(); + assertThat(env.matchesProfiles("d1")).isFalse(); + assertThat(env.matchesProfiles("a1 | a2")).isFalse(); } @Test @@ -127,7 +125,7 @@ protected String doGetDefaultProfilesProperty() { PropertySource propertySource = new MapPropertySource("test", values); env.getPropertySources().addFirst(propertySource); assertThat(env.getActiveProfiles()).isEmpty(); - assertThat(env.getDefaultProfiles()).containsExactly(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME); + assertThat(env.getDefaultProfiles()).containsExactly(DEFAULT_PROFILE); } @Test @@ -147,7 +145,7 @@ public CustomPropertySourcesPropertyResolver(PropertySources propertySources) { @Override @Nullable public String getProperty(String key) { - return super.getProperty(key)+"-test"; + return super.getProperty(key) + "-test"; } } @@ -165,8 +163,4 @@ protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySou assertThat(env.getProperty("spring")).isEqualTo("framework-test"); } - private Profiles defaultProfile() { - return Profiles.of(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME); - } - } diff --git a/spring-core/src/test/java/org/springframework/core/env/JOptCommandLinePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/JOptCommandLinePropertySourceTests.java index 8a11ab6ccebe..0aa2b3467084 100644 --- a/spring-core/src/test/java/org/springframework/core/env/JOptCommandLinePropertySourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/JOptCommandLinePropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,12 +25,13 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link JOptCommandLinePropertySource}. + * Tests for {@link JOptCommandLinePropertySource}. * * @author Chris Beams * @author Sam Brannen * @since 3.1 */ +@SuppressWarnings("deprecation") class JOptCommandLinePropertySourceTests { @Test diff --git a/spring-core/src/test/java/org/springframework/core/env/MutablePropertySourcesTests.java b/spring-core/src/test/java/org/springframework/core/env/MutablePropertySourcesTests.java index 7ff78bf4499e..077176f9de54 100644 --- a/spring-core/src/test/java/org/springframework/core/env/MutablePropertySourcesTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/MutablePropertySourcesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,12 +99,12 @@ void test() { assertThat(sources).hasSize(6); assertThat(sources.contains("a")).isFalse(); - assertThat((Object) sources.remove("a")).isNull(); + assertThat(sources.remove("a")).isNull(); assertThat(sources).hasSize(6); String bogusPS = "bogus"; - assertThatIllegalArgumentException().isThrownBy(() -> - sources.addAfter(bogusPS, new MockPropertySource("h"))) + assertThatIllegalArgumentException() + .isThrownBy(() -> sources.addAfter(bogusPS, new MockPropertySource("h"))) .withMessageContaining("does not exist"); sources.addFirst(new MockPropertySource("a")); @@ -121,16 +121,16 @@ void test() { sources.replace("a-replaced", new MockPropertySource("a")); - assertThatIllegalArgumentException().isThrownBy(() -> - sources.replace(bogusPS, new MockPropertySource("bogus-replaced"))) + assertThatIllegalArgumentException() + .isThrownBy(() -> sources.replace(bogusPS, new MockPropertySource("bogus-replaced"))) .withMessageContaining("does not exist"); - assertThatIllegalArgumentException().isThrownBy(() -> - sources.addBefore("b", new MockPropertySource("b"))) + assertThatIllegalArgumentException() + .isThrownBy(() -> sources.addBefore("b", new MockPropertySource("b"))) .withMessageContaining("cannot be added relative to itself"); - assertThatIllegalArgumentException().isThrownBy(() -> - sources.addAfter("b", new MockPropertySource("b"))) + assertThatIllegalArgumentException() + .isThrownBy(() -> sources.addAfter("b", new MockPropertySource("b"))) .withMessageContaining("cannot be added relative to itself"); } @@ -149,8 +149,7 @@ void iteratorContainsPropertySource() { assertThat(it.hasNext()).isTrue(); assertThat(it.next().getName()).isEqualTo("test"); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( - it::remove); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(it::remove); assertThat(it.hasNext()).isFalse(); } diff --git a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java index 421d26edb881..3c0154a6d9a2 100644 --- a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java @@ -272,6 +272,12 @@ void ofComplexExpressionWithoutSpaces() { assertComplexExpression(profiles); } + @Test + void ofComplexExpressionEnclosedInParentheses() { + Profiles profiles = Profiles.of("((spring & framework) | (spring & java))"); + assertComplexExpression(profiles); + } + private void assertComplexExpression(Profiles profiles) { assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); assertThat(profiles.matches(activeProfiles("spring", "framework"))).isTrue(); @@ -291,8 +297,27 @@ void sensibleToString() { assertThat(Profiles.of("spring")).hasToString("spring"); assertThat(Profiles.of("(spring & framework) | (spring & java)")).hasToString("(spring & framework) | (spring & java)"); assertThat(Profiles.of("(spring&framework)|(spring&java)")).hasToString("(spring&framework)|(spring&java)"); - assertThat(Profiles.of("spring & framework", "java | kotlin")).hasToString("spring & framework or java | kotlin"); - assertThat(Profiles.of("java | kotlin", "spring & framework")).hasToString("java | kotlin or spring & framework"); + assertThat(Profiles.of("spring & framework", "java | kotlin")).hasToString("(spring & framework) | (java | kotlin)"); + assertThat(Profiles.of("java | kotlin", "spring & framework")).hasToString("(java | kotlin) | (spring & framework)"); + assertThat(Profiles.of("java | kotlin", "spring & framework", "cat | dog")).hasToString("(java | kotlin) | (spring & framework) | (cat | dog)"); + } + + @Test + void toStringGeneratesValidCompositeProfileExpression() { + assertThatToStringGeneratesValidCompositeProfileExpression("spring"); + assertThatToStringGeneratesValidCompositeProfileExpression("(spring & kotlin) | (spring & java)"); + assertThatToStringGeneratesValidCompositeProfileExpression("spring & kotlin", "spring & java"); + assertThatToStringGeneratesValidCompositeProfileExpression("spring & kotlin", "spring & java", "cat | dog"); + } + + private static void assertThatToStringGeneratesValidCompositeProfileExpression(String... profileExpressions) { + Profiles profiles = Profiles.of(profileExpressions); + assertThat(profiles.matches(activeProfiles("spring", "java"))).isTrue(); + assertThat(profiles.matches(activeProfiles("kotlin"))).isFalse(); + + Profiles compositeProfiles = Profiles.of(profiles.toString()); + assertThat(compositeProfiles.matches(activeProfiles("spring", "java"))).isTrue(); + assertThat(compositeProfiles.matches(activeProfiles("kotlin"))).isFalse(); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourceTests.java index beeaf18e8ea4..748322e7ae4a 100644 --- a/spring-core/src/test/java/org/springframework/core/env/PropertySourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.core.env; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; @@ -27,7 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link PropertySource} implementations. + * Tests for {@link PropertySource} implementations. * * @author Chris Beams * @since 3.1 @@ -37,12 +36,8 @@ class PropertySourceTests { @Test @SuppressWarnings("serial") void equals() { - Map map1 = new HashMap<>() {{ - put("a", "b"); - }}; - Map map2 = new HashMap<>() {{ - put("c", "d"); - }}; + Map map1 = Map.of("a", "b"); + Map map2 = Map.of("c", "d"); Properties props1 = new Properties() {{ setProperty("a", "b"); }}; @@ -53,35 +48,30 @@ void equals() { MapPropertySource mps = new MapPropertySource("mps", map1); assertThat(mps).isEqualTo(mps); - assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("x", map1))).isTrue(); - assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("x", map2))).isTrue(); - assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("x", props1))).isTrue(); - assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("x", props2))).isTrue(); + assertThat(new MapPropertySource("x", map1)).isEqualTo(new MapPropertySource("x", map1)); + assertThat(new MapPropertySource("x", map1)).isEqualTo(new MapPropertySource("x", map2)); + assertThat(new MapPropertySource("x", map1)).isEqualTo(new PropertiesPropertySource("x", props1)); + assertThat(new MapPropertySource("x", map1)).isEqualTo(new PropertiesPropertySource("x", props2)); - assertThat(new MapPropertySource("x", map1).equals(new Object())).isFalse(); - assertThat(new MapPropertySource("x", map1).equals("x")).isFalse(); - assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("y", map1))).isFalse(); - assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("y", map2))).isFalse(); - assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("y", props1))).isFalse(); - assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("y", props2))).isFalse(); + assertThat(new MapPropertySource("x", map1)).isNotEqualTo(new Object()); + assertThat(new MapPropertySource("x", map1)).isNotEqualTo("x"); + assertThat(new MapPropertySource("x", map1)).isNotEqualTo(new MapPropertySource("y", map1)); + assertThat(new MapPropertySource("x", map1)).isNotEqualTo(new MapPropertySource("y", map2)); + assertThat(new MapPropertySource("x", map1)).isNotEqualTo(new PropertiesPropertySource("y", props1)); + assertThat(new MapPropertySource("x", map1)).isNotEqualTo(new PropertiesPropertySource("y", props2)); } @Test - @SuppressWarnings("serial") void collectionsOperations() { - Map map1 = new HashMap<>() {{ - put("a", "b"); - }}; - Map map2 = new HashMap<>() {{ - put("c", "d"); - }}; + Map map1 = Map.of("a", "b"); + Map map2 = Map.of("c", "d"); PropertySource ps1 = new MapPropertySource("ps1", map1); ps1.getSource(); List> propertySources = new ArrayList<>(); assertThat(propertySources.add(ps1)).isTrue(); - assertThat(propertySources.contains(ps1)).isTrue(); - assertThat(propertySources.contains(PropertySource.named("ps1"))).isTrue(); + assertThat(propertySources).contains(ps1); + assertThat(propertySources).contains(PropertySource.named("ps1")); PropertySource ps1replacement = new MapPropertySource("ps1", map2); // notice - different map assertThat(propertySources.add(ps1replacement)).isTrue(); // true because linkedlist allows duplicates diff --git a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java index 4e3f186f87b0..c5bbb89f8e5b 100644 --- a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link SimpleCommandLineArgsParser}. + * Tests for {@link SimpleCommandLineArgsParser}. * * @author Chris Beams * @author Sam Brannen diff --git a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLinePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLinePropertySourceTests.java index a4404332b7a3..86923da26eae 100644 --- a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLinePropertySourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLinePropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link SimpleCommandLinePropertySource}. + * Tests for {@link SimpleCommandLinePropertySource}. * * @author Chris Beams * @author Sam Brannen @@ -122,8 +122,7 @@ void covertNonOptionArgsToStringArrayAndList() { @SuppressWarnings("unchecked") List nonOptionArgsList = env.getProperty("nonOptionArgs", List.class); - assertThat(nonOptionArgsList.get(0)).isEqualTo("noa1"); - assertThat(nonOptionArgsList.get(1)).isEqualTo("noa2"); + assertThat(nonOptionArgsList).containsExactly("noa1", "noa2"); } } diff --git a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java index 36c7915ac096..a5f4cf43e2bd 100644 --- a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.core.env; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -31,7 +33,7 @@ import static org.springframework.core.env.AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME; /** - * Unit tests for {@link StandardEnvironment}. + * Tests for {@link StandardEnvironment}. * * @author Chris Beams * @author Juergen Hoeller @@ -300,6 +302,12 @@ void getSystemProperties() { assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isEqualTo(DISALLOWED_PROPERTY_VALUE); assertThat(systemProperties.get(STRING_PROPERTY_NAME)).isEqualTo(NON_STRING_PROPERTY_VALUE); assertThat(systemProperties.get(NON_STRING_PROPERTY_NAME)).isEqualTo(STRING_PROPERTY_VALUE); + + PropertiesPropertySource systemPropertySource = (PropertiesPropertySource) + environment.getPropertySources().get(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + Set expectedKeys = new HashSet<>(System.getProperties().stringPropertyNames()); + expectedKeys.add(STRING_PROPERTY_NAME); // filtered out by stringPropertyNames due to non-String value + assertThat(Set.of(systemPropertySource.getPropertyNames())).isEqualTo(expectedKeys); } finally { System.clearProperty(ALLOWED_PROPERTY_NAME); @@ -316,6 +324,7 @@ void getSystemEnvironment() { assertThat(System.getenv()).isSameAs(systemEnvironment); } + @Nested class GetActiveProfiles { @@ -365,6 +374,7 @@ void fromSystemProperties_withMultipleProfiles_withWhitespace() { } } + @Nested class AcceptsProfilesTests { @@ -447,33 +457,29 @@ void withProfileExpression() { environment.addActiveProfile("p2"); assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isTrue(); } - } + @Nested class MatchesProfilesTests { @Test - @SuppressWarnings("deprecation") void withEmptyArgumentList() { assertThatIllegalArgumentException().isThrownBy(environment::matchesProfiles); } @Test - @SuppressWarnings("deprecation") void withNullArgumentList() { assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles((String[]) null)); } @Test - @SuppressWarnings("deprecation") void withNullArgument() { assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles((String) null)); assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1", null)); } @Test - @SuppressWarnings("deprecation") void withEmptyArgument() { assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("")); assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1", "")); @@ -481,13 +487,11 @@ void withEmptyArgument() { } @Test - @SuppressWarnings("deprecation") void withInvalidNotOperator() { assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1", "!")); } @Test - @SuppressWarnings("deprecation") void withInvalidCompoundExpressionGrouping() { assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1 | p2 & p3")); assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1 & p2 | p3")); @@ -495,7 +499,6 @@ void withInvalidCompoundExpressionGrouping() { } @Test - @SuppressWarnings("deprecation") void activeProfileSetProgrammatically() { assertThat(environment.matchesProfiles("p1", "p2")).isFalse(); @@ -510,7 +513,6 @@ void activeProfileSetProgrammatically() { } @Test - @SuppressWarnings("deprecation") void activeProfileSetViaProperty() { assertThat(environment.matchesProfiles("p1")).isFalse(); @@ -519,7 +521,6 @@ void activeProfileSetViaProperty() { } @Test - @SuppressWarnings("deprecation") void defaultProfile() { assertThat(environment.matchesProfiles("pd")).isFalse(); @@ -532,7 +533,6 @@ void defaultProfile() { } @Test - @SuppressWarnings("deprecation") void withNotOperator() { assertThat(environment.matchesProfiles("p1")).isFalse(); assertThat(environment.matchesProfiles("!p1")).isTrue(); @@ -559,7 +559,6 @@ void withProfileExpressions() { assertThat(environment.matchesProfiles("p2 & (foo | p1)")).isTrue(); assertThat(environment.matchesProfiles("foo", "(p2 & p1)")).isTrue(); } - } } diff --git a/spring-core/src/test/java/org/springframework/core/env/SystemEnvironmentPropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/SystemEnvironmentPropertySourceTests.java index 2b4b1b5b4b4a..cf58ab6139f9 100644 --- a/spring-core/src/test/java/org/springframework/core/env/SystemEnvironmentPropertySourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/SystemEnvironmentPropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link SystemEnvironmentPropertySource}. + * Tests for {@link SystemEnvironmentPropertySource}. * * @author Chris Beams * @author Juergen Hoeller diff --git a/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java index f91e93d5f69a..09ce2063ec61 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ import static org.junit.jupiter.api.io.CleanupMode.NEVER; /** - * Unit tests for {@link ClassPathResource}. + * Tests for {@link ClassPathResource}. * *

      These also originally served as regression tests for the bugs described in * SPR-6888 and SPR-9413. @@ -214,7 +214,7 @@ void convertsToAbsolutePathForClassRelativeAccess() { @Test void directoryNotReadable() throws Exception { - Resource fileDir = new ClassPathResource("org/springframework/core"); + Resource fileDir = new ClassPathResource("example/type"); assertThat(fileDir.getURL()).asString().startsWith("file:"); assertThat(fileDir.exists()).isTrue(); assertThat(fileDir.isReadable()).isFalse(); diff --git a/spring-core/src/test/java/org/springframework/core/io/ModuleResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ModuleResourceTests.java new file mode 100644 index 000000000000..dc318713f282 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/ModuleResourceTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.io; + +import java.beans.Introspector; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ModuleResource}. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 6.1 + */ +class ModuleResourceTests { + + private static final String existingPath = "java/beans/Introspector.class"; + + private static final String nonExistingPath = "org/example/NonExistingClass.class"; + + + @Test + void existingClassFileResource() throws IOException { + // Check expected behavior of ClassPathResource first. + ClassPathResource cpr = new ClassPathResource(existingPath); + assertExistingResource(cpr); + assertThat(cpr.getDescription()).startsWith("class path resource").contains(cpr.getPath()); + + ModuleResource mr = new ModuleResource(Introspector.class.getModule(), existingPath); + assertExistingResource(mr); + assertThat(mr.getDescription()).startsWith("module resource").contains(mr.getModule().getName(), mr.getPath()); + assertThat(mr.getContentAsByteArray()).isEqualTo(cpr.getContentAsByteArray()); + assertThat(mr.contentLength()).isEqualTo(cpr.contentLength()); + } + + @Test + void nonExistingResource() { + ModuleResource mr = new ModuleResource(Introspector.class.getModule(), nonExistingPath); + assertThat(mr.exists()).isFalse(); + assertThat(mr.isReadable()).isFalse(); + assertThat(mr.isOpen()).isFalse(); + assertThat(mr.isFile()).isFalse(); + assertThat(mr.getFilename()).isEqualTo("NonExistingClass.class"); + assertThat(mr.getDescription()).startsWith("module resource").contains(mr.getModule().getName(), mr.getPath()); + + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(mr::getContentAsByteArray); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(mr::contentLength); + } + + @Test + void equalsAndHashCode() { + Resource resource1 = new ModuleResource(Introspector.class.getModule(), existingPath); + Resource resource2 = new ModuleResource(Introspector.class.getModule(), existingPath); + Resource resource3 = new ModuleResource(Introspector.class.getModule(), nonExistingPath); + + assertThat(resource1).isEqualTo(resource1); + assertThat(resource1).isEqualTo(resource2); + assertThat(resource2).isEqualTo(resource1); + assertThat(resource1).isNotEqualTo(resource3); + assertThat(resource1).hasSameHashCodeAs(resource2); + assertThat(resource1).doesNotHaveSameHashCodeAs(resource3); + } + + + private static void assertExistingResource(Resource resource) { + assertThat(resource.exists()).isTrue(); + assertThat(resource.isReadable()).isTrue(); + assertThat(resource.isOpen()).isFalse(); + assertThat(resource.isFile()).isFalse(); + assertThat(resource.getFilename()).isEqualTo("Introspector.class"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java index df00b8e8d5a6..c096c9c88765 100644 --- a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for the {@link PathResource} class. + * Tests for {@link PathResource}. * * @author Philippe Marschall * @author Phillip Webb @@ -161,13 +161,13 @@ void getInputStream() throws IOException { } @Test - void getInputStreamForDir() throws IOException { + void getInputStreamForDir() { PathResource resource = new PathResource(TEST_DIR); assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(resource::getInputStream); } @Test - void getInputStreamForNonExistingFile() throws IOException { + void getInputStreamForNonExistingFile() { PathResource resource = new PathResource(NON_EXISTING_FILE); assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(resource::getInputStream); } @@ -192,7 +192,7 @@ void getFile() throws IOException { } @Test - void getFileUnsupported() throws IOException { + void getFileUnsupported() { Path path = mock(); given(path.normalize()).willReturn(path); given(path.toFile()).willThrow(new UnsupportedOperationException()); @@ -222,13 +222,13 @@ void lastModified() throws IOException { } @Test - void createRelativeFromDir() throws IOException { + void createRelativeFromDir() { Resource resource = new PathResource(TEST_DIR).createRelative("example.properties"); assertThat(resource).isEqualTo(new PathResource(TEST_FILE)); } @Test - void createRelativeFromFile() throws IOException { + void createRelativeFromFile() { Resource resource = new PathResource(TEST_FILE).createRelative("../example.properties"); assertThat(resource).isEqualTo(new PathResource(TEST_FILE)); } @@ -316,7 +316,7 @@ void getReadableByteChannelForDir() throws IOException { } @Test - void getReadableByteChannelForNonExistingFile() throws IOException { + void getReadableByteChannelForNonExistingFile() { PathResource resource = new PathResource(NON_EXISTING_FILE); assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(resource::readableChannel); } diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java index d93c4bfba2c7..128d7a44cad9 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link ResourceEditor} class. + * Tests for {@link ResourceEditor}. * * @author Rick Evans * @author Arjen Poutsma @@ -73,7 +73,7 @@ void systemPropertyReplacement() { assertThat(resolved.getFilename()).isEqualTo("foo"); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } @@ -87,7 +87,7 @@ void systemPropertyReplacementWithUnresolvablePlaceholder() { assertThat(resolved.getFilename()).isEqualTo("foo-${bar}"); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } @@ -102,7 +102,7 @@ void strictSystemPropertyReplacementWithUnresolvablePlaceholder() { }); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java index 4fa131384b62..591254e678ff 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import okhttp3.mockwebserver.Dispatcher; @@ -56,7 +58,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; /** - * Unit tests for various {@link Resource} implementations. + * Tests for various {@link Resource} implementations. * * @author Juergen Hoeller * @author Chris Beams @@ -68,8 +70,8 @@ class ResourceTests { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceIsValid(Resource resource) throws Exception { - assertThat(resource.getFilename()).isEqualTo("Resource.class"); - assertThat(resource.getURL().getFile()).endsWith("Resource.class"); + assertThat(resource.getFilename()).isEqualTo("ResourceTests.class"); + assertThat(resource.getURL().getFile()).endsWith("ResourceTests.class"); assertThat(resource.exists()).isTrue(); assertThat(resource.isReadable()).isTrue(); assertThat(resource.contentLength()).isGreaterThan(0); @@ -80,9 +82,9 @@ void resourceIsValid(Resource resource) throws Exception { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceCreateRelative(Resource resource) throws Exception { - Resource relative1 = resource.createRelative("ClassPathResource.class"); - assertThat(relative1.getFilename()).isEqualTo("ClassPathResource.class"); - assertThat(relative1.getURL().getFile().endsWith("ClassPathResource.class")).isTrue(); + Resource relative1 = resource.createRelative("ClassPathResourceTests.class"); + assertThat(relative1.getFilename()).isEqualTo("ClassPathResourceTests.class"); + assertThat(relative1.getURL().getFile().endsWith("ClassPathResourceTests.class")).isTrue(); assertThat(relative1.exists()).isTrue(); assertThat(relative1.isReadable()).isTrue(); assertThat(relative1.contentLength()).isGreaterThan(0); @@ -92,9 +94,9 @@ void resourceCreateRelative(Resource resource) throws Exception { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceCreateRelativeWithFolder(Resource resource) throws Exception { - Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class"); - assertThat(relative2.getFilename()).isEqualTo("ResourcePatternResolver.class"); - assertThat(relative2.getURL().getFile()).endsWith("ResourcePatternResolver.class"); + Resource relative2 = resource.createRelative("support/PathMatchingResourcePatternResolverTests.class"); + assertThat(relative2.getFilename()).isEqualTo("PathMatchingResourcePatternResolverTests.class"); + assertThat(relative2.getURL().getFile()).endsWith("PathMatchingResourcePatternResolverTests.class"); assertThat(relative2.exists()).isTrue(); assertThat(relative2.isReadable()).isTrue(); assertThat(relative2.contentLength()).isGreaterThan(0); @@ -104,9 +106,9 @@ void resourceCreateRelativeWithFolder(Resource resource) throws Exception { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceCreateRelativeWithDotPath(Resource resource) throws Exception { - Resource relative3 = resource.createRelative("../SpringVersion.class"); - assertThat(relative3.getFilename()).isEqualTo("SpringVersion.class"); - assertThat(relative3.getURL().getFile()).endsWith("SpringVersion.class"); + Resource relative3 = resource.createRelative("../CollectionFactoryTests.class"); + assertThat(relative3.getFilename()).isEqualTo("CollectionFactoryTests.class"); + assertThat(relative3.getURL().getFile()).endsWith("CollectionFactoryTests.class"); assertThat(relative3.exists()).isTrue(); assertThat(relative3.isReadable()).isTrue(); assertThat(relative3.contentLength()).isGreaterThan(0); @@ -128,12 +130,12 @@ void resourceCreateRelativeUnknown(Resource resource) throws Exception { } private static Stream resource() throws URISyntaxException { - URL resourceClass = ResourceTests.class.getResource("Resource.class"); + URL resourceClass = ResourceTests.class.getResource("ResourceTests.class"); Path resourceClassFilePath = Paths.get(resourceClass.toURI()); return Stream.of( - arguments(named("ClassPathResource", new ClassPathResource("org/springframework/core/io/Resource.class"))), - arguments(named("ClassPathResource with ClassLoader", new ClassPathResource("org/springframework/core/io/Resource.class", ResourceTests.class.getClassLoader()))), - arguments(named("ClassPathResource with Class", new ClassPathResource("Resource.class", ResourceTests.class))), + arguments(named("ClassPathResource", new ClassPathResource("org/springframework/core/io/ResourceTests.class"))), + arguments(named("ClassPathResource with ClassLoader", new ClassPathResource("org/springframework/core/io/ResourceTests.class", ResourceTests.class.getClassLoader()))), + arguments(named("ClassPathResource with Class", new ClassPathResource("ResourceTests.class", ResourceTests.class))), arguments(named("FileSystemResource", new FileSystemResource(resourceClass.getFile()))), arguments(named("FileSystemResource with File", new FileSystemResource(new File(resourceClass.getFile())))), arguments(named("FileSystemResource with File path", new FileSystemResource(resourceClassFilePath))), @@ -171,7 +173,7 @@ void isNotOpen() { @Test void hasDescription() { Resource resource = new ByteArrayResource("testString".getBytes(), "my description"); - assertThat(resource.getDescription().contains("my description")).isTrue(); + assertThat(resource.getDescription()).contains("my description"); } } @@ -188,14 +190,21 @@ void hasContent() throws Exception { String content = FileCopyUtils.copyToString(new InputStreamReader(resource1.getInputStream())); assertThat(content).isEqualTo(testString); assertThat(new InputStreamResource(is)).isEqualTo(resource1); + assertThat(new InputStreamResource(() -> is)).isNotEqualTo(resource1); assertThatIllegalStateException().isThrownBy(resource1::getInputStream); Resource resource2 = new InputStreamResource(new ByteArrayInputStream(testBytes)); assertThat(resource2.getContentAsByteArray()).containsExactly(testBytes); assertThatIllegalStateException().isThrownBy(resource2::getContentAsByteArray); - Resource resource3 = new InputStreamResource(new ByteArrayInputStream(testBytes)); + AtomicBoolean obtained = new AtomicBoolean(); + Resource resource3 = new InputStreamResource(() -> { + obtained.set(true); + return new ByteArrayInputStream(testBytes); + }); + assertThat(obtained).isFalse(); assertThat(resource3.getContentAsString(StandardCharsets.US_ASCII)).isEqualTo(testString); + assertThat(obtained).isTrue(); assertThatIllegalStateException().isThrownBy(() -> resource3.getContentAsString(StandardCharsets.US_ASCII)); } @@ -205,13 +214,20 @@ void isOpen() { Resource resource = new InputStreamResource(is); assertThat(resource.exists()).isTrue(); assertThat(resource.isOpen()).isTrue(); + + resource = new InputStreamResource(() -> is); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isTrue(); } @Test void hasDescription() { InputStream is = new ByteArrayInputStream("testString".getBytes()); Resource resource = new InputStreamResource(is, "my description"); - assertThat(resource.getDescription().contains("my description")).isTrue(); + assertThat(resource.getDescription()).contains("my description"); + + resource = new InputStreamResource(() -> is, "my description"); + assertThat(resource.getDescription()).contains("my description"); } } @@ -221,29 +237,29 @@ class FileSystemResourceTests { @Test void sameResourceIsEqual() { - String file = getClass().getResource("Resource.class").getFile(); + String file = getClass().getResource("ResourceTests.class").getFile(); Resource resource = new FileSystemResource(file); assertThat(resource).isEqualTo(new FileSystemResource(file)); } @Test void sameResourceFromFileIsEqual() { - File file = new File(getClass().getResource("Resource.class").getFile()); + File file = new File(getClass().getResource("ResourceTests.class").getFile()); Resource resource = new FileSystemResource(file); assertThat(resource).isEqualTo(new FileSystemResource(file)); } @Test void sameResourceFromFilePathIsEqual() throws Exception { - Path filePath = Paths.get(getClass().getResource("Resource.class").toURI()); + Path filePath = Paths.get(getClass().getResource("ResourceTests.class").toURI()); Resource resource = new FileSystemResource(filePath); assertThat(resource).isEqualTo(new FileSystemResource(filePath)); } @Test void sameResourceFromDotPathIsEqual() { - Resource resource = new FileSystemResource("core/io/Resource.class"); - assertThat(new FileSystemResource("core/../core/io/./Resource.class")).isEqualTo(resource); + Resource resource = new FileSystemResource("core/io/ResourceTests.class"); + assertThat(new FileSystemResource("core/../core/io/./ResourceTests.class")).isEqualTo(resource); } @Test @@ -255,7 +271,7 @@ void relativeResourcesAreEqual() throws Exception { @Test void readableChannelProvidesContent() throws Exception { - Resource resource = new FileSystemResource(getClass().getResource("Resource.class").getFile()); + Resource resource = new FileSystemResource(getClass().getResource("ResourceTests.class").getFile()); try (ReadableByteChannel channel = resource.readableChannel()) { ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength()); channel.read(buffer); @@ -295,17 +311,34 @@ class UrlResourceTests { @Test void sameResourceWithRelativePathIsEqual() throws Exception { - Resource resource = new UrlResource("file:core/io/Resource.class"); - assertThat(new UrlResource("file:core/../core/io/./Resource.class")).isEqualTo(resource); + Resource resource = new UrlResource("file:core/io/ResourceTests.class"); + assertThat(new UrlResource("file:core/../core/io/./ResourceTests.class")).isEqualTo(resource); } @Test void filenameIsExtractedFromFilePath() throws Exception { + assertThat(new UrlResource("file:test?argh").getFilename()).isEqualTo("test"); + assertThat(new UrlResource("file:/test?argh").getFilename()).isEqualTo("test"); + assertThat(new UrlResource("file:test.txt?argh").getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource("file:/test.txt?argh").getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource("file:/dir/test?argh").getFilename()).isEqualTo("test"); assertThat(new UrlResource("file:/dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); assertThat(new UrlResource("file:\\dir\\test.txt?argh").getFilename()).isEqualTo("test.txt"); assertThat(new UrlResource("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); } + @Test + void filenameIsExtractedFromURL() throws Exception { + assertThat(new UrlResource(new URL("file:test?argh")).getFilename()).isEqualTo("test"); + assertThat(new UrlResource(new URL("file:/test?argh")).getFilename()).isEqualTo("test"); + assertThat(new UrlResource(new URL("file:test.txt?argh")).getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource(new URL("file:/test.txt?argh")).getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource(new URL("file:/dir/test?argh")).getFilename()).isEqualTo("test"); + assertThat(new UrlResource(new URL("file:/dir/test.txt?argh")).getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource(new URL("file:\\dir\\test.txt?argh")).getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource(new URL("file:\\dir/test.txt?argh")).getFilename()).isEqualTo("test.txt"); + } + @Test void filenameContainingHashTagIsExtractedFromFilePathUnencoded() throws Exception { String unencodedPath = "/dir/test#1.txt"; @@ -324,14 +357,14 @@ void filenameContainingHashTagIsExtractedFromFilePathUnencoded() throws Exceptio @Test void factoryMethodsProduceEqualResources() throws Exception { - Resource resource1 = new UrlResource("file:core/io/Resource.class"); - Resource resource2 = UrlResource.from("file:core/io/Resource.class"); + Resource resource1 = new UrlResource("file:core/io/ResourceTests.class"); + Resource resource2 = UrlResource.from("file:core/io/ResourceTests.class"); Resource resource3 = UrlResource.from(resource1.getURI()); assertThat(resource2.getURL()).isEqualTo(resource1.getURL()); assertThat(resource3.getURL()).isEqualTo(resource1.getURL()); - assertThat(UrlResource.from("file:core/../core/io/./Resource.class")).isEqualTo(resource1); + assertThat(UrlResource.from("file:core/../core/io/./ResourceTests.class")).isEqualTo(resource1); assertThat(UrlResource.from("file:/dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); assertThat(UrlResource.from("file:\\dir\\test.txt?argh").getFilename()).isEqualTo("test.txt"); assertThat(UrlResource.from("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); @@ -379,6 +412,19 @@ void canCustomizeHttpUrlConnectionForRead() throws Exception { assertThat(request.getHeader("Framework-Name")).isEqualTo("Spring"); } + @Test + void useUserInfoToSetBasicAuth() throws Exception { + startServer(); + UrlResource resource = new UrlResource("http://alice:secret@localhost:" + + this.server.getPort() + "/resource"); + assertThat(resource.getInputStream()).hasContent("Spring"); + RecordedRequest request = this.server.takeRequest(); + String authorization = request.getHeader("Authorization"); + assertThat(authorization).isNotNull().startsWith("Basic "); + assertThat(new String(Base64.getDecoder().decode( + authorization.substring(6)), StandardCharsets.ISO_8859_1)).isEqualTo("alice:secret"); + } + @AfterEach void shutdown() throws Exception { this.server.shutdown(); @@ -397,7 +443,7 @@ public CustomResource(String path) throws MalformedURLException { } @Override - protected void customizeConnection(HttpURLConnection con) throws IOException { + protected void customizeConnection(HttpURLConnection con) { con.setRequestProperty("Framework-Name", "Spring"); } } @@ -405,18 +451,17 @@ protected void customizeConnection(HttpURLConnection con) throws IOException { class ResourceDispatcher extends Dispatcher { @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + public MockResponse dispatch(RecordedRequest request) { if (request.getPath().equals("/resource")) { - switch (request.getMethod()) { - case "HEAD": - return new MockResponse() + return switch (request.getMethod()) { + case "HEAD" -> new MockResponse() .addHeader("Content-Length", "6"); - case "GET": - return new MockResponse() + case "GET" -> new MockResponse() .addHeader("Content-Length", "6") .addHeader("Content-Type", "text/plain") .setBody("Spring"); - } + default -> new MockResponse().setResponseCode(404); + }; } return new MockResponse().setResponseCode(404); } @@ -436,7 +481,6 @@ void missingResourceIsNotReadable() { public String getDescription() { return name; } - @Override public InputStream getInputStream() throws IOException { throw new FileNotFoundException(); @@ -462,7 +506,6 @@ void hasContentLength() throws Exception { public InputStream getInputStream() { return new ByteArrayInputStream(new byte[] {'a', 'b', 'c'}); } - @Override public String getDescription() { return ""; diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index cd3dd3c57986..a425f389b0d0 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.core.io.buffer; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -675,7 +674,39 @@ void toByteBufferDestination(DataBufferFactory bufferFactory) { } @ParameterizedDataBufferAllocatingTest - void readableByteBuffers(DataBufferFactory bufferFactory) throws IOException { + void readableByteBuffers(DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(3); + dataBuffer.write("abc".getBytes(StandardCharsets.UTF_8)); + dataBuffer.readPosition(1); + dataBuffer.writePosition(2); + + + byte[] result = new byte[1]; + try (var iterator = dataBuffer.readableByteBuffers()) { + assertThat(iterator).hasNext(); + int i = 0; + while (iterator.hasNext()) { + ByteBuffer byteBuffer = iterator.next(); + assertThat(byteBuffer.position()).isEqualTo(0); + assertThat(byteBuffer.limit()).isEqualTo(1); + assertThat(byteBuffer.capacity()).isEqualTo(1); + assertThat(byteBuffer.remaining()).isEqualTo(1); + + byteBuffer.get(result, i, 1); + + assertThat(iterator).isExhausted(); + } + } + + assertThat(result).containsExactly('b'); + + release(dataBuffer); + } + + @ParameterizedDataBufferAllocatingTest + void readableByteBuffersJoined(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; DataBuffer dataBuffer = this.bufferFactory.join(Arrays.asList(stringBuffer("a"), @@ -703,17 +734,26 @@ void readableByteBuffers(DataBufferFactory bufferFactory) throws IOException { void writableByteBuffers(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; - DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(1); + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(3); + dataBuffer.write("ab".getBytes(StandardCharsets.UTF_8)); + dataBuffer.readPosition(1); try (DataBuffer.ByteBufferIterator iterator = dataBuffer.writableByteBuffers()) { assertThat(iterator).hasNext(); ByteBuffer byteBuffer = iterator.next(); - byteBuffer.put((byte) 'a'); - dataBuffer.writePosition(1); + assertThat(byteBuffer.position()).isEqualTo(0); + assertThat(byteBuffer.limit()).isEqualTo(1); + assertThat(byteBuffer.capacity()).isEqualTo(1); + assertThat(byteBuffer.remaining()).isEqualTo(1); + + byteBuffer.put((byte) 'c'); + dataBuffer.writePosition(3); assertThat(iterator).isExhausted(); } - assertThat(dataBuffer.read()).isEqualTo((byte) 'a'); + byte[] result = new byte[2]; + dataBuffer.read(result); + assertThat(result).containsExactly('b', 'c'); release(dataBuffer); } @@ -945,4 +985,21 @@ void shouldHonorSourceBuffersReadPosition(DataBufferFactory bufferFactory) { assertThat(StandardCharsets.UTF_8.decode(byteBuffer).toString()).isEqualTo("b"); } + @ParameterizedDataBufferAllocatingTest // gh-31873 + void repeatedWrites(DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = bufferFactory.allocateBuffer(256); + String name = "Müller"; + int repeatCount = 19; + for (int i = 0; i < repeatCount; i++) { + buffer.write(name, StandardCharsets.UTF_8); + } + String result = buffer.toString(StandardCharsets.UTF_8); + String expected = name.repeat(repeatCount); + assertThat(result).isEqualTo(expected); + + release(buffer); + } + } diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java index 21f6116a515e..9ea04e339c62 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousFileChannel; @@ -34,11 +35,13 @@ import java.time.Duration; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import org.junit.jupiter.api.Test; import org.mockito.stubbing.Answer; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; @@ -52,6 +55,8 @@ import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; @@ -163,7 +168,7 @@ void readAsynchronousFileChannelPosition(DataBufferFactory bufferFactory) throws } @ParameterizedDataBufferAllocatingTest - void readAsynchronousFileChannelError(DataBufferFactory bufferFactory) throws Exception { + void readAsynchronousFileChannelError(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; AsynchronousFileChannel channel = mock(); @@ -232,7 +237,7 @@ void readPath(DataBufferFactory bufferFactory) throws Exception { } @ParameterizedDataBufferAllocatingTest - void readResource(DataBufferFactory bufferFactory) throws Exception { + void readResource(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; Flux flux = DataBufferUtils.read(this.resource, super.bufferFactory, 3); @@ -241,7 +246,7 @@ void readResource(DataBufferFactory bufferFactory) throws Exception { } @ParameterizedDataBufferAllocatingTest - void readResourcePosition(DataBufferFactory bufferFactory) throws Exception { + void readResourcePosition(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; Flux flux = DataBufferUtils.read(this.resource, 9, super.bufferFactory, 3); @@ -263,7 +268,7 @@ private void verifyReadData(Flux buffers) { } @ParameterizedDataBufferAllocatingTest - void readResourcePositionAndTakeUntil(DataBufferFactory bufferFactory) throws Exception { + void readResourcePositionAndTakeUntil(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; Resource resource = new ClassPathResource("DataBufferUtilsTests.txt", getClass()); @@ -280,7 +285,7 @@ void readResourcePositionAndTakeUntil(DataBufferFactory bufferFactory) throws Ex } @ParameterizedDataBufferAllocatingTest - void readByteArrayResourcePositionAndTakeUntil(DataBufferFactory bufferFactory) throws Exception { + void readByteArrayResourcePositionAndTakeUntil(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; Resource resource = new ByteArrayResource("foobarbazqux" .getBytes()); @@ -463,7 +468,6 @@ void writeAsynchronousFileChannelErrorInFlux(DataBufferFactory bufferFactory) th } @ParameterizedDataBufferAllocatingTest - @SuppressWarnings("unchecked") void writeAsynchronousFileChannelErrorInWrite(DataBufferFactory bufferFactory) throws Exception { super.bufferFactory = bufferFactory; @@ -543,6 +547,147 @@ void writePath(DataBufferFactory bufferFactory) throws Exception { assertThat(written).contains("foobar"); } + @ParameterizedDataBufferAllocatingTest + void outputStreamPublisher(DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + byte[] foo = "foo".getBytes(StandardCharsets.UTF_8); + byte[] bar = "bar".getBytes(StandardCharsets.UTF_8); + byte[] baz = "baz".getBytes(StandardCharsets.UTF_8); + + Publisher publisher = DataBufferUtils.outputStreamPublisher(outputStream -> { + try { + outputStream.write(foo); + outputStream.write(bar); + outputStream.write(baz); + } + catch (IOException ex) { + fail(ex.getMessage(), ex); + } + }, super.bufferFactory, Executors.newSingleThreadExecutor()); + + StepVerifier.create(publisher) + .consumeNextWith(stringConsumer("foobarbaz")) + .verifyComplete(); + } + + @ParameterizedDataBufferAllocatingTest + void outputStreamPublisherFlush(DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + byte[] foo = "foo".getBytes(StandardCharsets.UTF_8); + byte[] bar = "bar".getBytes(StandardCharsets.UTF_8); + byte[] baz = "baz".getBytes(StandardCharsets.UTF_8); + + Publisher publisher = DataBufferUtils.outputStreamPublisher(outputStream -> { + try { + outputStream.write(foo); + outputStream.flush(); + outputStream.write(bar); + outputStream.flush(); + outputStream.write(baz); + outputStream.flush(); + } + catch (IOException ex) { + fail(ex.getMessage(), ex); + } + }, super.bufferFactory, Executors.newSingleThreadExecutor()); + + StepVerifier.create(publisher) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .consumeNextWith(stringConsumer("baz")) + .verifyComplete(); + } + + @ParameterizedDataBufferAllocatingTest + void outputStreamPublisherChunkSize(DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + byte[] foo = "foo".getBytes(StandardCharsets.UTF_8); + byte[] bar = "bar".getBytes(StandardCharsets.UTF_8); + byte[] baz = "baz".getBytes(StandardCharsets.UTF_8); + + Publisher publisher = DataBufferUtils.outputStreamPublisher(outputStream -> { + try { + outputStream.write(foo); + outputStream.write(bar); + outputStream.write(baz); + } + catch (IOException ex) { + fail(ex.getMessage(), ex); + } + }, super.bufferFactory, Executors.newSingleThreadExecutor(), 3); + + StepVerifier.create(publisher) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .consumeNextWith(stringConsumer("baz")) + .verifyComplete(); + } + + @ParameterizedDataBufferAllocatingTest + void outputStreamPublisherCancel(DataBufferFactory bufferFactory) throws InterruptedException { + super.bufferFactory = bufferFactory; + + byte[] foo = "foo".getBytes(StandardCharsets.UTF_8); + byte[] bar = "bar".getBytes(StandardCharsets.UTF_8); + + CountDownLatch latch = new CountDownLatch(1); + + Publisher publisher = DataBufferUtils.outputStreamPublisher(outputStream -> { + try { + assertThatIOException() + .isThrownBy(() -> { + outputStream.write(foo); + outputStream.flush(); + outputStream.write(bar); + outputStream.flush(); + }) + .withMessage("Subscription has been terminated"); + } + finally { + latch.countDown(); + } + }, super.bufferFactory, Executors.newSingleThreadExecutor()); + + StepVerifier.create(publisher, 1) + .consumeNextWith(stringConsumer("foo")) + .thenCancel() + .verify(); + + latch.await(); + } + + @ParameterizedDataBufferAllocatingTest + void outputStreamPublisherClosed(DataBufferFactory bufferFactory) throws InterruptedException { + super.bufferFactory = bufferFactory; + + CountDownLatch latch = new CountDownLatch(1); + + Publisher publisher = DataBufferUtils.outputStreamPublisher(outputStream -> { + try { + OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + writer.write("foo"); + writer.close(); + assertThatIOException().isThrownBy(() -> writer.write("bar")) + .withMessage("Stream closed"); + } + catch (IOException ex) { + fail(ex.getMessage(), ex); + } + finally { + latch.countDown(); + } + }, super.bufferFactory, Executors.newSingleThreadExecutor()); + + StepVerifier.create(publisher) + .consumeNextWith(stringConsumer("foo")) + .verifyComplete(); + + latch.await(); + } + @ParameterizedDataBufferAllocatingTest void readAndWriteByteChannel(DataBufferFactory bufferFactory) throws Exception { super.bufferFactory = bufferFactory; diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java index 20a48616b82b..a4ec71fbcea1 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,11 +24,12 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * Unit tests for {@link LimitedDataBufferList}. + * Tests for {@link LimitedDataBufferList}. + * * @author Rossen Stoyanchev * @since 5.1.11 */ -public class LimitedDataBufferListTests { +class LimitedDataBufferListTests { @Test void limitEnforced() { diff --git a/spring-core/src/test/java/org/springframework/core/io/support/EncodedResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/support/EncodedResourceTests.java index 76eae72684ab..a5b9af9cae2e 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/EncodedResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/EncodedResourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.core.io.support; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -26,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link EncodedResource}. + * Tests for {@link EncodedResource}. * * @author Sam Brannen * @since 3.2.14 @@ -35,15 +36,15 @@ class EncodedResourceTests { private static final String UTF8 = "UTF-8"; private static final String UTF16 = "UTF-16"; - private static final Charset UTF8_CS = Charset.forName(UTF8); - private static final Charset UTF16_CS = Charset.forName(UTF16); + private static final Charset UTF8_CS = StandardCharsets.UTF_8; + private static final Charset UTF16_CS = StandardCharsets.UTF_16; private final Resource resource = new DescriptiveResource("test"); @Test void equalsWithNullOtherObject() { - assertThat(new EncodedResource(resource).equals(null)).isFalse(); + assertThat(new EncodedResource(resource)).isNotEqualTo(null); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index b234739a270a..30f1ac941f31 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,14 +51,14 @@ */ class PathMatchingResourcePatternResolverTests { - private static final String[] CLASSES_IN_CORE_IO_SUPPORT = { "EncodedResource.class", + private static final String[] CLASSES_IN_CORE_IO_SUPPORT = {"EncodedResource.class", "LocalizedResourceHelper.class", "PathMatchingResourcePatternResolver.class", "PropertiesLoaderSupport.class", "PropertiesLoaderUtils.class", "ResourceArrayPropertyEditor.class", "ResourcePatternResolver.class", - "ResourcePatternUtils.class", "SpringFactoriesLoader.class" }; + "ResourcePatternUtils.class", "SpringFactoriesLoader.class"}; - private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = { "PathMatchingResourcePatternResolverTests.class" }; + private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = {"PathMatchingResourcePatternResolverTests.class"}; - private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = { "NonNull.class", "NonNullApi.class", "Nullable.class" }; + private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = {"NonNull.class", "NonNullApi.class", "Nullable.class"}; private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); @@ -200,7 +200,7 @@ private List getSubPathsIgnoringClassFilesEtc(String pattern, String pat } @Test - void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() { + void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() { Path testResourcesDir = Paths.get("src/test/resources").toAbsolutePath(); String pattern = String.format("file:%s/scanned-resources/**", testResourcesDir); String pathPrefix = ".+?resources/"; @@ -329,8 +329,8 @@ private void assertExactSubPaths(String pattern, String pathPrefix, String... su } private String getPath(Resource resource) { - // Tests fail if we use resouce.getURL().getPath(). They would also fail on Mac OS when - // using resouce.getURI().getPath() if the resource paths are not Unicode normalized. + // Tests fail if we use resource.getURL().getPath(). They would also fail on macOS when + // using resource.getURI().getPath() if the resource paths are not Unicode normalized. // // On the JVM, all tests should pass when using resouce.getFile().getPath(); however, // we use FileSystemResource#getPath since this test class is sometimes run within a diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java index a19e88529b57..221736ce14c7 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ import static org.assertj.core.api.Assertions.assertThatNoException; /** - * Unit tests for {@link PropertySourceProcessor}. + * Tests for {@link PropertySourceProcessor}. * * @author Sam Brannen * @since 6.0.12 @@ -137,7 +137,7 @@ private void assertProcessorIgnoresFailure(Class createPropertySource(String name, EncodedResource resource) throws IOException { + public PropertySource createPropertySource(String name, EncodedResource resource) { throw new IllegalArgumentException("bogus"); } } diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java index 4bb19cdc33de..eabd4e46bf65 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,20 @@ import org.junit.jupiter.api.Test; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileUrlResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** + * Tests for {@link ResourceArrayPropertyEditor}. + * * @author Dave Syer * @author Juergen Hoeller + * @author Yanming Zhou + * @author Stephane Nicoll */ class ResourceArrayPropertyEditorTests { @@ -64,7 +70,7 @@ void systemPropertyReplacement() { assertThat(resources[0].getFilename()).isEqualTo("foo"); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } @@ -79,8 +85,35 @@ void strictSystemPropertyReplacementWithUnresolvablePlaceholder() { editor.setAsText("${test.prop}-${bar}")); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } + @Test + void commaDelimitedResourcesWithSingleResource() { + PropertyEditor editor = new ResourceArrayPropertyEditor(); + editor.setAsText("classpath:org/springframework/core/io/support/ResourceArrayPropertyEditor.class,file:/test.txt"); + Resource[] resources = (Resource[]) editor.getValue(); + assertThat(resources).isNotNull(); + assertThat(resources[0]).isInstanceOfSatisfying(ClassPathResource.class, + resource -> assertThat(resource.exists()).isTrue()); + assertThat(resources[1]).isInstanceOfSatisfying(FileUrlResource.class, + resource -> assertThat(resource.getFilename()).isEqualTo("test.txt")); + } + + @Test + void commaDelimitedResourcesWithMultipleResources() { + PropertyEditor editor = new ResourceArrayPropertyEditor(); + editor.setAsText("file:/test.txt, classpath:org/springframework/core/io/support/test-resources/*.txt"); + Resource[] resources = (Resource[]) editor.getValue(); + assertThat(resources).isNotNull(); + assertThat(resources[0]).isInstanceOfSatisfying(FileUrlResource.class, + resource -> assertThat(resource.getFilename()).isEqualTo("test.txt")); + assertThat(resources).anySatisfy(candidate -> + assertThat(candidate.getFilename()).isEqualTo("resource1.txt")); + assertThat(resources).anySatisfy(candidate -> + assertThat(candidate.getFilename()).isEqualTo("resource2.txt")); + assertThat(resources).hasSize(3); + } + } diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourcePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourcePropertySourceTests.java index df4a13c4ee2b..ecc131ff685d 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/ResourcePropertySourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourcePropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link ResourcePropertySource}. + * Tests for {@link ResourcePropertySource}. * * @author Chris Beams * @author Sam Brannen diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourceRegionTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourceRegionTests.java index 271cfbed3f85..728350053064 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/ResourceRegionTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourceRegionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for the {@link ResourceRegion} class. + * Tests for {@link ResourceRegion}. * * @author Brian Clozel */ diff --git a/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java index 28b420ad4164..541da8c9f20a 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,9 +84,7 @@ void loadWhenNoRegisteredImplementationsReturnsEmptyList() { @Test void loadWhenDuplicateRegistrationsPresentReturnsListInCorrectOrder() { List factories = SpringFactoriesLoader.forDefaultResourceLocation().load(DummyFactory.class); - assertThat(factories).hasSize(2); - assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class); - assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class); + assertThat(factories).hasExactlyElementsOfTypes(MyDummyFactory1.class, MyDummyFactory2.class); } @Test @@ -118,10 +116,8 @@ void loadWithArgumentResolverWhenNoDefaultConstructor() { ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected"); List factories = SpringFactoriesLoader.forDefaultResourceLocation(LimitedClassLoader.constructorArgumentFactories) .load(DummyFactory.class, resolver); - assertThat(factories).hasSize(3); - assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class); - assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class); - assertThat(factories.get(2)).isInstanceOf(ConstructorArgsDummyFactory.class); + assertThat(factories).hasExactlyElementsOfTypes(MyDummyFactory1.class, MyDummyFactory2.class, + ConstructorArgsDummyFactory.class); assertThat(factories).extracting(DummyFactory::getString).containsExactly("Foo", "Bar", "injected"); } @@ -142,18 +138,14 @@ void loadWithLoggingFailureHandlerWhenMissingArgumentDropsItem() { FailureHandler failureHandler = FailureHandler.logging(logger); List factories = SpringFactoriesLoader.forDefaultResourceLocation(LimitedClassLoader.multipleArgumentFactories) .load(DummyFactory.class, failureHandler); - assertThat(factories).hasSize(2); - assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class); - assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class); + assertThat(factories).hasExactlyElementsOfTypes(MyDummyFactory1.class, MyDummyFactory2.class); } @Test void loadFactoriesLoadsFromDefaultLocation() { List factories = SpringFactoriesLoader.loadFactories( DummyFactory.class, null); - assertThat(factories).hasSize(2); - assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class); - assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class); + assertThat(factories).hasExactlyElementsOfTypes(MyDummyFactory1.class, MyDummyFactory2.class); } @Test @@ -167,8 +159,7 @@ void loadForResourceLocationWhenLocationDoesNotExistReturnsEmptyList() { void loadForResourceLocationLoadsFactories() { List factories = SpringFactoriesLoader.forResourceLocation( "META-INF/custom/custom-spring.factories").load(DummyFactory.class); - assertThat(factories).hasSize(1); - assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class); + assertThat(factories).hasExactlyElementsOfTypes(MyDummyFactory1.class); } @Test @@ -220,8 +211,7 @@ void handleMessageReturnsHandlerThatAcceptsMessage() { RuntimeException cause = new RuntimeException(); handler.handleFailure(DummyFactory.class, MyDummyFactory1.class.getName(), cause); assertThat(failures).containsExactly(cause); - assertThat(messages).hasSize(1); - assertThat(messages.get(0)).startsWith("Unable to instantiate factory class"); + assertThat(messages).singleElement().asString().startsWith("Unable to instantiate factory class"); } } diff --git a/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.java b/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.java index ca70a15ebf0b..993c910a6c48 100644 --- a/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.java +++ b/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,11 @@ /** - * Unit tests for {@link CompositeLog}. + * Tests for {@link CompositeLog}. + * * @author Rossen Stoyanchev */ -public class CompositeLogTests { +class CompositeLogTests { private final Log logger1 = mock(); diff --git a/spring-core/src/test/java/org/springframework/core/serializer/SerializationConverterTests.java b/spring-core/src/test/java/org/springframework/core/serializer/SerializationConverterTests.java index f57abf820e7c..82374b3199c2 100644 --- a/spring-core/src/test/java/org/springframework/core/serializer/SerializationConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/serializer/SerializationConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,27 @@ package org.springframework.core.serializer; +import java.io.ByteArrayInputStream; import java.io.NotSerializableException; import java.io.Serializable; import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.springframework.core.ConfigurableObjectInputStream; import org.springframework.core.serializer.support.DeserializingConverter; import org.springframework.core.serializer.support.SerializationFailedException; import org.springframework.core.serializer.support.SerializingConverter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.mockito.BDDMockito.given; /** + * Tests for {@link SerializingConverter} and {@link DeserializingConverter}. + * * @author Gary Russell * @author Mark Fisher * @since 3.0.5 @@ -36,43 +44,84 @@ class SerializationConverterTests { @Test - void serializeAndDeserializeString() { + void serializeAndDeserializeStringWithDefaultSerializer() { SerializingConverter toBytes = new SerializingConverter(); byte[] bytes = toBytes.convert("Testing"); DeserializingConverter fromBytes = new DeserializingConverter(); assertThat(fromBytes.convert(bytes)).isEqualTo("Testing"); } + @Test + void serializeAndDeserializeStringWithExplicitSerializer() { + SerializingConverter toBytes = new SerializingConverter(new DefaultSerializer()); + byte[] bytes = toBytes.convert("Testing"); + DeserializingConverter fromBytes = new DeserializingConverter(); + assertThat(fromBytes.convert(bytes)).isEqualTo("Testing"); + } + @Test void nonSerializableObject() { SerializingConverter toBytes = new SerializingConverter(); - assertThatExceptionOfType(SerializationFailedException.class).isThrownBy(() -> - toBytes.convert(new Object())) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(SerializationFailedException.class) + .isThrownBy(() -> toBytes.convert(new Object())) + .havingCause() + .isInstanceOf(IllegalArgumentException.class) + .withMessageContaining("requires a Serializable payload"); } @Test void nonSerializableField() { SerializingConverter toBytes = new SerializingConverter(); - assertThatExceptionOfType(SerializationFailedException.class).isThrownBy(() -> - toBytes.convert(new UnSerializable())) - .withCauseInstanceOf(NotSerializableException.class); + assertThatExceptionOfType(SerializationFailedException.class) + .isThrownBy(() -> toBytes.convert(new UnSerializable())) + .withCauseInstanceOf(NotSerializableException.class); } @Test void deserializationFailure() { DeserializingConverter fromBytes = new DeserializingConverter(); - assertThatExceptionOfType(SerializationFailedException.class).isThrownBy(() -> - fromBytes.convert("Junk".getBytes())); + assertThatExceptionOfType(SerializationFailedException.class) + .isThrownBy(() -> fromBytes.convert("Junk".getBytes())); + } + + @Test + void deserializationWithExplicitClassLoader() { + DeserializingConverter fromBytes = new DeserializingConverter(getClass().getClassLoader()); + SerializingConverter toBytes = new SerializingConverter(); + String expected = "SPRING FRAMEWORK"; + assertThat(fromBytes.convert(toBytes.convert(expected))).isEqualTo(expected); + } + + @Test + void deserializationWithExplicitDeserializer() { + DeserializingConverter fromBytes = new DeserializingConverter(new DefaultDeserializer()); + SerializingConverter toBytes = new SerializingConverter(); + String expected = "SPRING FRAMEWORK"; + assertThat(fromBytes.convert(toBytes.convert(expected))).isEqualTo(expected); + } + + @Test + void deserializationIOException() { + ClassNotFoundException classNotFoundException = new ClassNotFoundException(); + try (MockedConstruction mocked = + Mockito.mockConstruction(ConfigurableObjectInputStream.class, + (mock, context) -> given(mock.readObject()).willThrow(classNotFoundException))) { + DefaultDeserializer defaultSerializer = new DefaultDeserializer(getClass().getClassLoader()); + assertThat(mocked).isNotNull(); + assertThatIOException() + .isThrownBy(() -> defaultSerializer.deserialize(new ByteArrayInputStream("test".getBytes()))) + .withMessage("Failed to deserialize object type") + .havingCause().isSameAs(classNotFoundException); + } } - class UnSerializable implements Serializable { + static class UnSerializable implements Serializable { private static final long serialVersionUID = 1L; @SuppressWarnings({"unused", "serial"}) - private Object object; + private Object object = new Object(); } } diff --git a/spring-core/src/test/java/org/springframework/core/serializer/SerializerTests.java b/spring-core/src/test/java/org/springframework/core/serializer/SerializerTests.java new file mode 100644 index 000000000000..f26f9b5b7b86 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/serializer/SerializerTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.serializer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.serializer.support.SerializationDelegate; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Tests for {@link Serializer}, {@link Deserializer}, and {@link SerializationDelegate}. + * + * @since 6.1 + */ +class SerializerTests { + + private static final String SPRING_FRAMEWORK = "Spring Framework"; + + + @Test + void serializeToByteArray() throws IOException { + + class SpyStringSerializer implements Serializer { + + String expectedObject; + OutputStream expectedOutputStream; + + @Override + public void serialize(String object, OutputStream outputStream) { + this.expectedObject = object; + this.expectedOutputStream = outputStream; + } + } + + SpyStringSerializer serializer = new SpyStringSerializer(); + serializer.serializeToByteArray(SPRING_FRAMEWORK); + assertThat(serializer.expectedObject).isEqualTo(SPRING_FRAMEWORK); + assertThat(serializer.expectedOutputStream).isNotNull(); + } + + @Test + void deserializeToByteArray() throws IOException { + + class SpyStringDeserializer implements Deserializer { + + InputStream expectedInputStream; + + @Override + public String deserialize(InputStream inputStream) { + this.expectedInputStream = inputStream; + return SPRING_FRAMEWORK; + } + } + + SpyStringDeserializer deserializer = new SpyStringDeserializer(); + Object deserializedObj = deserializer.deserializeFromByteArray(SPRING_FRAMEWORK.getBytes()); + assertThat(deserializedObj).isEqualTo(SPRING_FRAMEWORK); + assertThat(deserializer.expectedInputStream).isNotNull(); + } + + @Test + void serializationDelegateWithExplicitSerializerAndDeserializer() throws IOException { + SerializationDelegate delegate = new SerializationDelegate(new DefaultSerializer(), new DefaultDeserializer()); + byte[] serializedObj = delegate.serializeToByteArray(SPRING_FRAMEWORK); + Object deserializedObj = delegate.deserialize(new ByteArrayInputStream(serializedObj)); + assertThat(deserializedObj).isEqualTo(SPRING_FRAMEWORK); + } + + @Test + void serializationDelegateWithExplicitClassLoader() throws IOException { + SerializationDelegate delegate = new SerializationDelegate(getClass().getClassLoader()); + byte[] serializedObj = delegate.serializeToByteArray(SPRING_FRAMEWORK); + Object deserializedObj = delegate.deserialize(new ByteArrayInputStream(serializedObj)); + assertThat(deserializedObj).isEqualTo(SPRING_FRAMEWORK); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/style/DefaultValueStylerTests.java b/spring-core/src/test/java/org/springframework/core/style/DefaultValueStylerTests.java index 38f75f0b3fda..0ad780e92a58 100644 --- a/spring-core/src/test/java/org/springframework/core/style/DefaultValueStylerTests.java +++ b/spring-core/src/test/java/org/springframework/core/style/DefaultValueStylerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link DefaultValueStyler}. + * Tests for {@link DefaultValueStyler}. * * @since 5.2 */ diff --git a/spring-core/src/test/java/org/springframework/core/style/SimpleValueStylerTests.java b/spring-core/src/test/java/org/springframework/core/style/SimpleValueStylerTests.java index 83f56e428884..44e52028737b 100644 --- a/spring-core/src/test/java/org/springframework/core/style/SimpleValueStylerTests.java +++ b/spring-core/src/test/java/org/springframework/core/style/SimpleValueStylerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link SimpleValueStyler}. + * Tests for {@link SimpleValueStyler}. * * @author Sam Brannen * @since 6.0 @@ -41,7 +41,7 @@ class CommonStyling { private final SimpleValueStyler styler = new SimpleValueStyler(); @Test - void styleBasics() throws NoSuchMethodException { + void styleBasics() { assertThat(styler.style(null)).isEqualTo("null"); assertThat(styler.style(true)).isEqualTo("true"); assertThat(styler.style(99.9)).isEqualTo("99.9"); diff --git a/spring-core/src/test/java/org/springframework/core/style/ToStringCreatorTests.java b/spring-core/src/test/java/org/springframework/core/style/ToStringCreatorTests.java index 32b03b08407a..c041c3c41e15 100644 --- a/spring-core/src/test/java/org/springframework/core/style/ToStringCreatorTests.java +++ b/spring-core/src/test/java/org/springframework/core/style/ToStringCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link ToStringCreator}. + * Tests for {@link ToStringCreator}. * * @author Keith Donald * @author Sam Brannen diff --git a/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java index fbdf751435e6..c7f4bd9d3b47 100644 --- a/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java +++ b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,17 +33,18 @@ class SimpleAsyncTaskExecutorTests { @Test void cannotExecuteWhenConcurrencyIsSwitchedOff() { - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); - executor.setConcurrencyLimit(ConcurrencyThrottleSupport.NO_CONCURRENCY); - assertThat(executor.isThrottleActive()).isTrue(); - assertThatIllegalStateException().isThrownBy(() -> - executor.execute(new NoOpRunnable())); + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { + executor.setConcurrencyLimit(ConcurrencyThrottleSupport.NO_CONCURRENCY); + assertThat(executor.isThrottleActive()).isTrue(); + assertThatIllegalStateException().isThrownBy(() -> executor.execute(new NoOpRunnable())); + } } @Test void throttleIsNotActiveByDefault() { - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); - assertThat(executor.isThrottleActive()).as("Concurrency throttle must not default to being active (on)").isFalse(); + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { + assertThat(executor.isThrottleActive()).as("Concurrency throttle must not default to being active (on)").isFalse(); + } } @Test @@ -67,8 +68,9 @@ void threadFactoryOverridesDefaults() { @Test void throwsExceptionWhenSuppliedWithNullRunnable() { - assertThatIllegalArgumentException().isThrownBy(() -> - new SimpleAsyncTaskExecutor().execute(null)); + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { + assertThatIllegalArgumentException().isThrownBy(() -> executor.execute(null)); + } } private void executeAndWait(SimpleAsyncTaskExecutor executor, Runnable task, Object monitor) { @@ -92,7 +94,7 @@ public void run() { } - private static abstract class AbstractNotifyingRunnable implements Runnable { + private abstract static class AbstractNotifyingRunnable implements Runnable { private final Object monitor; diff --git a/spring-core/src/test/java/org/springframework/core/task/support/CompositeTaskDecoratorTests.java b/spring-core/src/test/java/org/springframework/core/task/support/CompositeTaskDecoratorTests.java new file mode 100644 index 000000000000..93093575cd06 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/task/support/CompositeTaskDecoratorTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task.support; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.core.task.TaskDecorator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link CompositeTaskDecorator}. + * + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + */ +class CompositeTaskDecoratorTests { + + @Test + void createWithNullCollection() { + assertThatIllegalArgumentException().isThrownBy(() -> new CompositeTaskDecorator(null)) + .withMessage("TaskDecorators must not be null"); + } + + @Test + void decorateWithNullRunnable() { + CompositeTaskDecorator taskDecorator = new CompositeTaskDecorator(List.of()); + assertThatIllegalArgumentException().isThrownBy(() -> taskDecorator.decorate(null)) + .withMessage("Runnable must not be null"); + } + + @Test + void decorate() { + TaskDecorator first = mockNoOpTaskDecorator(); + TaskDecorator second = mockNoOpTaskDecorator(); + TaskDecorator third = mockNoOpTaskDecorator(); + CompositeTaskDecorator taskDecorator = new CompositeTaskDecorator(List.of(first, second, third)); + Runnable runnable = mock(); + taskDecorator.decorate(runnable); + InOrder ordered = inOrder(first, second, third); + ordered.verify(first).decorate(runnable); + ordered.verify(second).decorate(runnable); + ordered.verify(third).decorate(runnable); + } + + @Test + void decorateReusesResultOfPreviousRun() { + Runnable original = mock(); + Runnable firstDecorated = mock(); + TaskDecorator first = mock(); + given(first.decorate(original)).willReturn(firstDecorated); + Runnable secondDecorated = mock(); + TaskDecorator second = mock(); + given(second.decorate(firstDecorated)).willReturn(secondDecorated); + Runnable result = new CompositeTaskDecorator(List.of(first, second)).decorate(original); + assertThat(result).isSameAs(secondDecorated); + verify(first).decorate(original); + verify(second).decorate(firstDecorated); + } + + private TaskDecorator mockNoOpTaskDecorator() { + TaskDecorator mock = mock(); + given(mock.decorate(any())).willAnswer(invocation -> invocation.getArguments()[0]); + return mock; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/task/support/ContextPropagatingTaskDecoratorTests.java b/spring-core/src/test/java/org/springframework/core/task/support/ContextPropagatingTaskDecoratorTests.java new file mode 100644 index 000000000000..397f97297fef --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/task/support/ContextPropagatingTaskDecoratorTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task.support; + +import java.util.concurrent.atomic.AtomicReference; + +import io.micrometer.context.ContextRegistry; +import io.micrometer.context.ContextSnapshotFactory; +import io.micrometer.context.ThreadLocalAccessor; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ContextPropagatingTaskDecorator}. + * @author Brian Clozel + */ +class ContextPropagatingTaskDecoratorTests { + + @Test + void shouldPropagateContextInTaskExecution() throws Exception { + AtomicReference actual = new AtomicReference<>(""); + ContextRegistry registry = new ContextRegistry(); + registry.registerThreadLocalAccessor(new TestThreadLocalAccessor()); + ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder().contextRegistry(registry).build(); + + Runnable task = () -> actual.set(TestThreadLocalHolder.getValue()); + TestThreadLocalHolder.setValue("expected"); + + Thread execution = new Thread(new ContextPropagatingTaskDecorator(snapshotFactory).decorate(task)); + execution.start(); + execution.join(); + assertThat(actual.get()).isEqualTo("expected"); + TestThreadLocalHolder.reset(); + } + + static class TestThreadLocalHolder { + + private static final ThreadLocal holder = new ThreadLocal<>(); + + static void setValue(String value) { + holder.set(value); + } + + static String getValue() { + return holder.get(); + } + + static void reset() { + holder.remove(); + } + + } + + static class TestThreadLocalAccessor implements ThreadLocalAccessor { + + static final String KEY = "test.threadlocal"; + + @Override + public Object key() { + return KEY; + } + + @Override + public String getValue() { + return TestThreadLocalHolder.getValue(); + } + + @Override + public void setValue(String value) { + TestThreadLocalHolder.setValue(value); + } + + @Override + public void setValue() { + TestThreadLocalHolder.reset(); + } + + @Override + public void restore(String previousValue) { + setValue(previousValue); + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupTests.java b/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupTests.java index 0a04ce34c32a..65e742cd9911 100644 --- a/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupTests.java +++ b/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.core.testfixture; import java.util.Arrays; +import java.util.Objects; import java.util.Set; import org.junit.jupiter.api.AfterEach; @@ -52,12 +53,7 @@ void trackOriginalTestGroups() { @AfterEach void restoreOriginalTestGroups() { - if (this.originalTestGroups != null) { - setTestGroups(this.originalTestGroups); - } - else { - setTestGroups(""); - } + setTestGroups(Objects.requireNonNullElse(this.originalTestGroups, "")); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java index 850319061d17..cc966f179955 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,30 +38,30 @@ public abstract class AbstractAnnotationMetadataTests { @Test - public void verifyEquals() throws Exception { + void verifyEquals() { AnnotationMetadata testClass1 = get(TestClass.class); AnnotationMetadata testClass2 = get(TestClass.class); AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); - assertThat(testClass1.equals(null)).isFalse(); + assertThat(testClass1).isNotEqualTo(null); - assertThat(testClass1.equals(testClass1)).isTrue(); - assertThat(testClass2.equals(testClass2)).isTrue(); - assertThat(testClass1.equals(testClass2)).isTrue(); - assertThat(testClass2.equals(testClass1)).isTrue(); + assertThat(testClass1).isEqualTo(testClass1); + assertThat(testClass2).isEqualTo(testClass2); + assertThat(testClass1).isEqualTo(testClass2); + assertThat(testClass2).isEqualTo(testClass1); - assertThat(testMemberClass1.equals(testMemberClass1)).isTrue(); - assertThat(testMemberClass2.equals(testMemberClass2)).isTrue(); - assertThat(testMemberClass1.equals(testMemberClass2)).isTrue(); - assertThat(testMemberClass2.equals(testMemberClass1)).isTrue(); + assertThat(testMemberClass1).isEqualTo(testMemberClass1); + assertThat(testMemberClass2).isEqualTo(testMemberClass2); + assertThat(testMemberClass1).isEqualTo(testMemberClass2); + assertThat(testMemberClass2).isEqualTo(testMemberClass1); - assertThat(testClass1.equals(testMemberClass1)).isFalse(); - assertThat(testMemberClass1.equals(testClass1)).isFalse(); + assertThat(testClass1).isNotEqualTo(testMemberClass1); + assertThat(testMemberClass1).isNotEqualTo(testClass1); } @Test - public void verifyHashCode() throws Exception { + void verifyHashCode() { AnnotationMetadata testClass1 = get(TestClass.class); AnnotationMetadata testClass2 = get(TestClass.class); AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); @@ -74,106 +74,106 @@ public void verifyHashCode() throws Exception { } @Test - public void verifyToString() throws Exception { + void verifyToString() { assertThat(get(TestClass.class).toString()).isEqualTo(TestClass.class.getName()); } @Test - public void getClassNameReturnsClassName() { + void getClassNameReturnsClassName() { assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName()); } @Test - public void isInterfaceWhenInterfaceReturnsTrue() { + void isInterfaceWhenInterfaceReturnsTrue() { assertThat(get(TestInterface.class).isInterface()).isTrue(); assertThat(get(TestAnnotation.class).isInterface()).isTrue(); } @Test - public void isInterfaceWhenNotInterfaceReturnsFalse() { + void isInterfaceWhenNotInterfaceReturnsFalse() { assertThat(get(TestClass.class).isInterface()).isFalse(); } @Test - public void isAnnotationWhenAnnotationReturnsTrue() { + void isAnnotationWhenAnnotationReturnsTrue() { assertThat(get(TestAnnotation.class).isAnnotation()).isTrue(); } @Test - public void isAnnotationWhenNotAnnotationReturnsFalse() { + void isAnnotationWhenNotAnnotationReturnsFalse() { assertThat(get(TestClass.class).isAnnotation()).isFalse(); assertThat(get(TestInterface.class).isAnnotation()).isFalse(); } @Test - public void isFinalWhenFinalReturnsTrue() { + void isFinalWhenFinalReturnsTrue() { assertThat(get(TestFinalClass.class).isFinal()).isTrue(); } @Test - public void isFinalWhenNonFinalReturnsFalse() { + void isFinalWhenNonFinalReturnsFalse() { assertThat(get(TestClass.class).isFinal()).isFalse(); } @Test - public void isIndependentWhenIndependentReturnsTrue() { + void isIndependentWhenIndependentReturnsTrue() { assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue(); assertThat(get(TestClass.class).isIndependent()).isTrue(); } @Test - public void isIndependentWhenNotIndependentReturnsFalse() { + void isIndependentWhenNotIndependentReturnsFalse() { assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse(); } @Test - public void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() { + void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() { assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo( AbstractAnnotationMetadataTests.class.getName()); } @Test - public void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() { + void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() { assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull(); } @Test - public void getSuperClassNameWhenHasSuperClassReturnsName() { + void getSuperClassNameWhenHasSuperClassReturnsName() { assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName()); assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName()); } @Test - public void getSuperClassNameWhenHasNoSuperClassReturnsNull() { + void getSuperClassNameWhenHasNoSuperClassReturnsNull() { assertThat(get(Object.class).getSuperClassName()).isNull(); assertThat(get(TestInterface.class).getSuperClassName()).isNull(); assertThat(get(TestSubInterface.class).getSuperClassName()).isNull(); } @Test - public void getInterfaceNamesWhenHasInterfacesReturnsNames() { + void getInterfaceNamesWhenHasInterfacesReturnsNames() { assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); } @Test - public void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() { + void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() { assertThat(get(TestClass.class).getInterfaceNames()).isEmpty(); } @Test - public void getMemberClassNamesWhenHasMemberClassesReturnsNames() { + void getMemberClassNamesWhenHasMemberClassesReturnsNames() { assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder( TestMemberClassInnerClass.class.getName(), TestMemberClassInnerInterface.class.getName()); } @Test - public void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { + void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { assertThat(get(TestClass.class).getMemberClassNames()).isEmpty(); } @Test - public void getAnnotationsReturnsDirectAnnotations() { + void getAnnotationsReturnsDirectAnnotations() { assertThat(get(WithDirectAnnotations.class).getAnnotations().stream()) .filteredOn(MergedAnnotation::isDirectlyPresent) .extracting(a -> a.getType().getName()) @@ -181,28 +181,28 @@ public void getAnnotationsReturnsDirectAnnotations() { } @Test - public void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { + void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue(); } @Test - public void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { + void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue(); } @Test - public void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { assertThat(get(TestClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse(); } @Test - public void getAnnotationAttributesReturnsAttributes() { + void getAnnotationAttributesReturnsAttributes() { assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName())) .containsOnly(entry("name", "test"), entry("size", 1)); } @Test - public void getAllAnnotationAttributesReturnsAllAttributes() { + void getAllAnnotationAttributesReturnsAllAttributes() { MultiValueMap attributes = get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName()); assertThat(attributes).containsOnlyKeys("name", "size"); @@ -211,69 +211,69 @@ public void getAllAnnotationAttributesReturnsAllAttributes() { } @Test - public void getAnnotationTypesReturnsDirectAnnotations() { + void getAnnotationTypesReturnsDirectAnnotations() { AnnotationMetadata metadata = get(WithDirectAnnotations.class); assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); } @Test - public void getMetaAnnotationTypesReturnsMetaAnnotations() { + void getMetaAnnotationTypesReturnsMetaAnnotations() { AnnotationMetadata metadata = get(WithMetaAnnotations.class); assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName())) .containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); } @Test - public void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() { + void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() { assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue(); } @Test - public void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() { + void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() { assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse(); assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse(); } @Test - public void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { assertThat(get(TestClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse(); } @Test - public void hasMetaAnnotationWhenMatchesDirectReturnsFalse() { + void hasMetaAnnotationWhenMatchesDirectReturnsFalse() { assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse(); } @Test - public void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() { + void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() { assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue(); assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue(); } @Test - public void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { assertThat(get(TestClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse(); } @Test - public void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() { + void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() { assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue(); } @Test - public void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() { + void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() { assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue(); } @Test - public void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() { + void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() { assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse(); assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse(); } @Test - public void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() { + void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() { assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName())) .extracting(MethodMetadata::getMethodName) .containsExactlyInAnyOrder("direct", "meta"); diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java index e728a95073a5..91ced1dc1802 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,30 +38,30 @@ public abstract class AbstractMethodMetadataTests { @Test - public void verifyEquals() throws Exception { + void verifyEquals() { MethodMetadata withMethod1 = getTagged(WithMethod.class); MethodMetadata withMethod2 = getTagged(WithMethod.class); MethodMetadata withMethodWithTwoArguments1 = getTagged(WithMethodWithTwoArguments.class); MethodMetadata withMethodWithTwoArguments2 = getTagged(WithMethodWithTwoArguments.class); - assertThat(withMethod1.equals(null)).isFalse(); + assertThat(withMethod1).isNotEqualTo(null); - assertThat(withMethod1.equals(withMethod1)).isTrue(); - assertThat(withMethod2.equals(withMethod2)).isTrue(); - assertThat(withMethod1.equals(withMethod2)).isTrue(); - assertThat(withMethod2.equals(withMethod1)).isTrue(); + assertThat(withMethod1).isEqualTo(withMethod1); + assertThat(withMethod2).isEqualTo(withMethod2); + assertThat(withMethod1).isEqualTo(withMethod2); + assertThat(withMethod2).isEqualTo(withMethod1); - assertThat(withMethodWithTwoArguments1.equals(withMethodWithTwoArguments1)).isTrue(); - assertThat(withMethodWithTwoArguments2.equals(withMethodWithTwoArguments2)).isTrue(); - assertThat(withMethodWithTwoArguments1.equals(withMethodWithTwoArguments2)).isTrue(); - assertThat(withMethodWithTwoArguments2.equals(withMethodWithTwoArguments1)).isTrue(); + assertThat(withMethodWithTwoArguments1).isEqualTo(withMethodWithTwoArguments1); + assertThat(withMethodWithTwoArguments2).isEqualTo(withMethodWithTwoArguments2); + assertThat(withMethodWithTwoArguments1).isEqualTo(withMethodWithTwoArguments2); + assertThat(withMethodWithTwoArguments2).isEqualTo(withMethodWithTwoArguments1); - assertThat(withMethod1.equals(withMethodWithTwoArguments1)).isFalse(); - assertThat(withMethodWithTwoArguments1.equals(withMethod1)).isFalse(); + assertThat(withMethod1).isNotEqualTo(withMethodWithTwoArguments1); + assertThat(withMethodWithTwoArguments1).isNotEqualTo(withMethod1); } @Test - public void verifyHashCode() throws Exception { + void verifyHashCode() { MethodMetadata withMethod1 = getTagged(WithMethod.class); MethodMetadata withMethod2 = getTagged(WithMethod.class); MethodMetadata withMethodWithTwoArguments1 = getTagged(WithMethodWithTwoArguments.class); @@ -74,7 +74,7 @@ public void verifyHashCode() throws Exception { } @Test - public void verifyToString() throws Exception { + void verifyToString() { assertThat(getTagged(WithMethod.class).toString()) .endsWith(WithMethod.class.getName() + ".test()"); @@ -86,66 +86,66 @@ public void verifyToString() throws Exception { } @Test - public void getMethodNameReturnsMethodName() { + void getMethodNameReturnsMethodName() { assertThat(getTagged(WithMethod.class).getMethodName()).isEqualTo("test"); } @Test - public void getDeclaringClassReturnsDeclaringClass() { + void getDeclaringClassReturnsDeclaringClass() { assertThat(getTagged(WithMethod.class).getDeclaringClassName()).isEqualTo( WithMethod.class.getName()); } @Test - public void getReturnTypeReturnsReturnType() { + void getReturnTypeReturnsReturnType() { assertThat(getTagged(WithMethod.class).getReturnTypeName()).isEqualTo( String.class.getName()); } @Test - public void isAbstractWhenAbstractReturnsTrue() { + void isAbstractWhenAbstractReturnsTrue() { assertThat(getTagged(WithAbstractMethod.class).isAbstract()).isTrue(); } @Test - public void isAbstractWhenNotAbstractReturnsFalse() { + void isAbstractWhenNotAbstractReturnsFalse() { assertThat(getTagged(WithMethod.class).isAbstract()).isFalse(); } @Test - public void isStatusWhenStaticReturnsTrue() { + void isStatusWhenStaticReturnsTrue() { assertThat(getTagged(WithStaticMethod.class).isStatic()).isTrue(); } @Test - public void isStaticWhenNotStaticReturnsFalse() { + void isStaticWhenNotStaticReturnsFalse() { assertThat(getTagged(WithMethod.class).isStatic()).isFalse(); } @Test - public void isFinalWhenFinalReturnsTrue() { + void isFinalWhenFinalReturnsTrue() { assertThat(getTagged(WithFinalMethod.class).isFinal()).isTrue(); } @Test - public void isFinalWhenNonFinalReturnsFalse() { + void isFinalWhenNonFinalReturnsFalse() { assertThat(getTagged(WithMethod.class).isFinal()).isFalse(); } @Test - public void isOverridableWhenOverridableReturnsTrue() { + void isOverridableWhenOverridableReturnsTrue() { assertThat(getTagged(WithMethod.class).isOverridable()).isTrue(); } @Test - public void isOverridableWhenNonOverridableReturnsFalse() { + void isOverridableWhenNonOverridableReturnsFalse() { assertThat(getTagged(WithStaticMethod.class).isOverridable()).isFalse(); assertThat(getTagged(WithFinalMethod.class).isOverridable()).isFalse(); assertThat(getTagged(WithPrivateMethod.class).isOverridable()).isFalse(); } @Test - public void getAnnotationsReturnsDirectAnnotations() { + void getAnnotationsReturnsDirectAnnotations() { MethodMetadata metadata = getTagged(WithDirectAnnotation.class); assertThat(metadata.getAnnotations().stream().filter( MergedAnnotation::isDirectlyPresent).map( @@ -155,32 +155,32 @@ public void getAnnotationsReturnsDirectAnnotations() { } @Test - public void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { + void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { assertThat(getTagged(WithDirectAnnotation.class).isAnnotated( DirectAnnotation.class.getName())).isTrue(); } @Test - public void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { + void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { assertThat(getTagged(WithMetaAnnotation.class).isAnnotated( DirectAnnotation.class.getName())).isTrue(); } @Test - public void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { assertThat(getTagged(WithMethod.class).isAnnotated( DirectAnnotation.class.getName())).isFalse(); } @Test - public void getAnnotationAttributesReturnsAttributes() { + void getAnnotationAttributesReturnsAttributes() { assertThat(getTagged(WithAnnotationAttributes.class).getAnnotationAttributes( AnnotationAttributes.class.getName())).containsOnly(entry("name", "test"), entry("size", 1)); } @Test - public void getAllAnnotationAttributesReturnsAllAttributes() { + void getAllAnnotationAttributesReturnsAllAttributes() { MultiValueMap attributes = getTagged(WithMetaAnnotationAttributes.class) .getAllAnnotationAttributes(AnnotationAttributes.class.getName()); assertThat(attributes).containsOnlyKeys("name", "size"); @@ -263,13 +263,13 @@ public final String test() { public static class WithPrivateMethod { @Tag - private final String test() { + private String test() { return ""; } } - public static abstract class WithDirectAnnotation { + public abstract static class WithDirectAnnotation { @Tag @DirectAnnotation @@ -277,7 +277,7 @@ public static abstract class WithDirectAnnotation { } - public static abstract class WithMetaAnnotation { + public abstract static class WithMetaAnnotation { @Tag @MetaAnnotation @@ -294,7 +294,7 @@ public static abstract class WithMetaAnnotation { @interface MetaAnnotation { } - public static abstract class WithAnnotationAttributes { + public abstract static class WithAnnotationAttributes { @Tag @AnnotationAttributes(name = "test", size = 1) @@ -302,7 +302,7 @@ public static abstract class WithAnnotationAttributes { } - public static abstract class WithMetaAnnotationAttributes { + public abstract static class WithMetaAnnotationAttributes { @Tag @MetaAnnotationAttributes1 diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index a3bb98d251ae..497217db8c39 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -32,6 +33,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.testfixture.stereotype.Component; import org.springframework.core.type.classreading.MetadataReader; @@ -204,7 +206,7 @@ void metaAnnotationOverridesUsingStandardAnnotationMetadata() { } @Test - void metaAnnotationOverridesUsingAnnotationMetadataReadingVisitor() throws Exception { + void metaAnnotationOverridesUsingSimpleAnnotationMetadata() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(ComposedConfigurationWithAttributeOverridesClass.class.getName()); AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); @@ -214,8 +216,8 @@ void metaAnnotationOverridesUsingAnnotationMetadataReadingVisitor() throws Excep private void assertMetaAnnotationOverrides(AnnotationMetadata metadata) { AnnotationAttributes attributes = (AnnotationAttributes) metadata.getAnnotationAttributes( TestComponentScan.class.getName(), false); + assertThat(attributes.getStringArray("value")).containsExactly("org.example.componentscan"); assertThat(attributes.getStringArray("basePackages")).containsExactly("org.example.componentscan"); - assertThat(attributes.getStringArray("value")).isEmpty(); assertThat(attributes.getClassArray("basePackageClasses")).isEmpty(); } @@ -226,7 +228,7 @@ void multipleAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetada } @Test // SPR-11649 - void multipleAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { + void multipleAnnotationsWithIdenticalAttributeNamesUsingSimpleAnnotationMetadata() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedAnnotationsClass.class.getName()); AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); @@ -240,13 +242,117 @@ void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStanda } @Test // SPR-11649 - void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { + void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingSimpleAnnotationMetadata() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedComposedAnnotationClass.class.getName()); AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); } + @Test // gh-31041 + void multipleComposedRepeatableAnnotationsUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleComposedRepeatableAnnotationsClass.class); + assertRepeatableAnnotations(metadata); + } + + @Test // gh-31041 + void multipleComposedRepeatableAnnotationsUsingSimpleAnnotationMetadata() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleComposedRepeatableAnnotationsClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertRepeatableAnnotations(metadata); + } + + @Test // gh-31074 + void multipleComposedRepeatableAnnotationsSortedByReversedMetaDistanceUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleComposedRepeatableAnnotationsClass.class); + assertRepeatableAnnotationsSortedByReversedMetaDistance(metadata); + } + + @Test // gh-31074 + void multipleComposedRepeatableAnnotationsSortedByReversedMetaDistanceUsingSimpleAnnotationMetadata() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleComposedRepeatableAnnotationsClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertRepeatableAnnotationsSortedByReversedMetaDistance(metadata); + } + + @Test // gh-31041 + void multipleRepeatableAnnotationsInContainersUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleRepeatableAnnotationsInContainersClass.class); + assertRepeatableAnnotations(metadata); + } + + @Test // gh-31041 + void multipleRepeatableAnnotationsInContainersUsingSimpleAnnotationMetadata() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleRepeatableAnnotationsInContainersClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertRepeatableAnnotations(metadata); + } + + /** + * Tests {@code AnnotatedElementUtils#getMergedRepeatableAnnotations()} variants to ensure that + * {@link AnnotationMetadata#getMergedRepeatableAnnotationAttributes(Class, Class, boolean)} + * behaves the same. + */ + @Test // gh-31041 + void multipleComposedRepeatableAnnotationsUsingAnnotatedElementUtils() { + Class element = MultipleComposedRepeatableAnnotationsClass.class; + + Set annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class); + assertRepeatableAnnotations(annotations); + + annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class, TestComponentScans.class); + assertRepeatableAnnotations(annotations); + } + + /** + * Tests {@code AnnotatedElementUtils#getMergedRepeatableAnnotations()} variants to ensure that + * {@link AnnotationMetadata#getMergedRepeatableAnnotationAttributes(Class, Class, boolean)} + * behaves the same. + */ + @Test // gh-31041 + void multipleRepeatableAnnotationsInContainersUsingAnnotatedElementUtils() { + Class element = MultipleRepeatableAnnotationsInContainersClass.class; + + Set annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class); + assertRepeatableAnnotations(annotations); + + annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class, TestComponentScans.class); + assertRepeatableAnnotations(annotations); + } + + private static void assertRepeatableAnnotations(AnnotationMetadata metadata) { + Set attributesSet = + metadata.getMergedRepeatableAnnotationAttributes(TestComponentScan.class, TestComponentScans.class, true); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("value")).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("basePackages")).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("basePackageClasses")).flatMap(Arrays::stream)) + .containsExactly("java.lang.String", "java.lang.Integer"); + } + + private static void assertRepeatableAnnotationsSortedByReversedMetaDistance(AnnotationMetadata metadata) { + // Note: although the real @ComponentScan annotation is not looked up using + // "sortByReversedMetaDistance" semantics, we can still use @TestComponentScan + // to verify the expected behavior. + Set attributesSet = + metadata.getMergedRepeatableAnnotationAttributes(TestComponentScan.class, TestComponentScans.class, false, true); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("value")).flatMap(Arrays::stream)) + .containsExactly("C", "D", "A", "B"); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("basePackages")).flatMap(Arrays::stream)) + .containsExactly("C", "D", "A", "B"); + } + + private static void assertRepeatableAnnotations(Set annotations) { + assertThat(annotations.stream().map(TestComponentScan::value).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + assertThat(annotations.stream().map(TestComponentScan::basePackages).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + } + @Test void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() { AnnotationMetadata metadata = AnnotationMetadata.introspect(NamedComposedAnnotationExtended.class); @@ -254,7 +360,7 @@ void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStand } @Test - void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { + void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingSimpleAnnotationMetadata() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedComposedAnnotationExtended.class.getName()); AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); @@ -319,7 +425,7 @@ private void doTestAnnotationInfo(AnnotationMetadata metadata) { List allMeta = method.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", "meta"))); allMeta = method.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("additional"); - assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct"))); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(List.of("direct"))); assertThat(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName())).isTrue(); @@ -534,10 +640,20 @@ private static class AnnotatedComponentSubClass extends AnnotatedComponent { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) + public @interface TestComponentScans { + + TestComponentScan[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Repeatable(TestComponentScans.class) public @interface TestComponentScan { + @AliasFor("basePackages") String[] value() default {}; + @AliasFor("value") String[] basePackages() default {}; Class[] basePackageClasses() default {}; @@ -558,6 +674,40 @@ private static class AnnotatedComponentSubClass extends AnnotatedComponent { public static class ComposedConfigurationWithAttributeOverridesClass { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @TestComponentScan("C") + public @interface ScanPackageC { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @TestComponentScan("D") + public @interface ScanPackageD { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @TestComponentScans({ + @TestComponentScan("C"), + @TestComponentScan("D") + }) + public @interface ScanPackagesCandD { + } + + @TestComponentScan(basePackages = "A", basePackageClasses = String.class) + @ScanPackageC + @ScanPackageD + @TestComponentScan(basePackages = "B", basePackageClasses = Integer.class) + static class MultipleComposedRepeatableAnnotationsClass { + } + + @TestComponentScan(basePackages = "A", basePackageClasses = String.class) + @ScanPackagesCandD + @TestComponentScans(@TestComponentScan(basePackages = "B", basePackageClasses = Integer.class)) + static class MultipleRepeatableAnnotationsInContainersClass { + } + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface NamedAnnotation1 { diff --git a/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java b/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java index 981e880866da..67647495ccce 100644 --- a/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; /** - * Unit tests for checking the behaviour of {@link CachingMetadataReaderFactory} under + * Tests for checking the behaviour of {@link CachingMetadataReaderFactory} under * load. If the cache is not controlled, this test should fail with an out of memory * exception around entry 5k. * diff --git a/spring-core/src/test/java/org/springframework/core/type/Scope.java b/spring-core/src/test/java/org/springframework/core/type/Scope.java index eb581928a96b..4b10cac0c66d 100644 --- a/spring-core/src/test/java/org/springframework/core/type/Scope.java +++ b/spring-core/src/test/java/org/springframework/core/type/Scope.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ /** * Copy of the {@code @Scope} annotation for testing purposes. */ -@Target({ ElementType.TYPE, ElementType.METHOD }) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Scope { diff --git a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java index 8a6bc82921ba..ee33545735b4 100644 --- a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java +++ b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link AntPathMatcher}. + * Tests for {@link AntPathMatcher}. * * @author Alef Arendsen * @author Seth Ladd @@ -143,7 +143,7 @@ void matchWithNullPath() { // SPR-14247 @Test - void matchWithTrimTokensEnabled() throws Exception { + void matchWithTrimTokensEnabled() { pathMatcher.setTrimTokens(true); assertThat(pathMatcher.match("/foo/bar", "/foo /bar")).isTrue(); @@ -299,7 +299,7 @@ void uniqueDeliminator() { } @Test - void extractPathWithinPattern() throws Exception { + void extractPathWithinPattern() { assertThat(pathMatcher.extractPathWithinPattern("/docs/commit.html", "/docs/commit.html")).isEmpty(); assertThat(pathMatcher.extractPathWithinPattern("/docs/*", "/docs/cvs/commit")).isEqualTo("cvs/commit"); @@ -325,7 +325,7 @@ void extractPathWithinPattern() throws Exception { } @Test - void extractUriTemplateVariables() throws Exception { + void extractUriTemplateVariables() { Map result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}", "/hotels/1"); assertThat(result).isEqualTo(Collections.singletonMap("hotel", "1")); @@ -513,74 +513,63 @@ void patternComparatorSort() { paths.add(null); paths.add("/hotels/new"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new"); - assertThat(paths.get(1)).isNull(); + assertThat(paths).containsExactly("/hotels/new", null); paths.clear(); paths.add("/hotels/new"); paths.add(null); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new"); - assertThat(paths.get(1)).isNull(); + assertThat(paths).containsExactly("/hotels/new", null); paths.clear(); paths.add("/hotels/*"); paths.add("/hotels/new"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new"); - assertThat(paths.get(1)).isEqualTo("/hotels/*"); + assertThat(paths).containsExactly("/hotels/new", "/hotels/*"); paths.clear(); paths.add("/hotels/new"); paths.add("/hotels/*"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new"); - assertThat(paths.get(1)).isEqualTo("/hotels/*"); + assertThat(paths).containsExactly("/hotels/new", "/hotels/*"); paths.clear(); paths.add("/hotels/**"); paths.add("/hotels/*"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/*"); - assertThat(paths.get(1)).isEqualTo("/hotels/**"); + assertThat(paths).containsExactly("/hotels/*", "/hotels/**"); paths.clear(); paths.add("/hotels/*"); paths.add("/hotels/**"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/*"); - assertThat(paths.get(1)).isEqualTo("/hotels/**"); + assertThat(paths).containsExactly("/hotels/*", "/hotels/**"); paths.clear(); paths.add("/hotels/{hotel}"); paths.add("/hotels/new"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new"); - assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); + assertThat(paths).containsExactly("/hotels/new", "/hotels/{hotel}"); paths.clear(); paths.add("/hotels/new"); paths.add("/hotels/{hotel}"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new"); - assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); + assertThat(paths).containsExactly("/hotels/new", "/hotels/{hotel}"); paths.clear(); paths.add("/hotels/*"); paths.add("/hotels/{hotel}"); paths.add("/hotels/new"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new"); - assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); - assertThat(paths.get(2)).isEqualTo("/hotels/*"); + assertThat(paths).containsExactly("/hotels/new", "/hotels/{hotel}", "/hotels/*"); paths.clear(); paths.add("/hotels/ne*"); paths.add("/hotels/n*"); Collections.shuffle(paths); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/ne*"); - assertThat(paths.get(1)).isEqualTo("/hotels/n*"); + assertThat(paths).containsExactly("/hotels/ne*", "/hotels/n*"); paths.clear(); comparator = pathMatcher.getPatternComparator("/hotels/new.html"); @@ -588,16 +577,14 @@ void patternComparatorSort() { paths.add("/hotels/{hotel}"); Collections.shuffle(paths); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/hotels/new.*"); - assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); + assertThat(paths).containsExactly("/hotels/new.*", "/hotels/{hotel}"); paths.clear(); comparator = pathMatcher.getPatternComparator("/web/endUser/action/login.html"); paths.add("/**/login.*"); paths.add("/**/endUser/action/login.*"); paths.sort(comparator); - assertThat(paths.get(0)).isEqualTo("/**/endUser/action/login.*"); - assertThat(paths.get(1)).isEqualTo("/**/login.*"); + assertThat(paths).containsExactly("/**/endUser/action/login.*", "/**/login.*"); paths.clear(); } @@ -628,7 +615,7 @@ void defaultCacheSetting() { pathMatcher.match("test" + i, "test"); } // Cache turned off because it went beyond the threshold - assertThat(pathMatcher.stringMatcherCache.isEmpty()).isTrue(); + assertThat(pathMatcher.stringMatcherCache).isEmpty(); } @Test @@ -680,7 +667,7 @@ void creatingStringMatchersIfPatternPrefixCannotDetermineIfPathMatch() { void cachePatternsSetToFalse() { pathMatcher.setCachePatterns(false); match(); - assertThat(pathMatcher.stringMatcherCache.isEmpty()).isTrue(); + assertThat(pathMatcher.stringMatcherCache).isEmpty(); } @Test diff --git a/spring-core/src/test/java/org/springframework/util/AssertTests.java b/spring-core/src/test/java/org/springframework/util/AssertTests.java index 603dbffebc87..13c69e6088b0 100644 --- a/spring-core/src/test/java/org/springframework/util/AssertTests.java +++ b/spring-core/src/test/java/org/springframework/util/AssertTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for the {@link Assert} class. + * Tests for {@link Assert}. * * @author Keith Donald * @author Erwin Vervaet diff --git a/spring-core/src/test/java/org/springframework/util/AutoPopulatingListTests.java b/spring-core/src/test/java/org/springframework/util/AutoPopulatingListTests.java index 1be14c376474..09f89f1b2fea 100644 --- a/spring-core/src/test/java/org/springframework/util/AutoPopulatingListTests.java +++ b/spring-core/src/test/java/org/springframework/util/AutoPopulatingListTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.ArrayList; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.core.testfixture.io.SerializationTestUtils; @@ -32,22 +33,22 @@ class AutoPopulatingListTests { @Test - void withClass() throws Exception { + void withClass() { doTestWithClass(new AutoPopulatingList<>(TestObject.class)); } @Test - void withClassAndUserSuppliedBackingList() throws Exception { - doTestWithClass(new AutoPopulatingList(new ArrayList<>(), TestObject.class)); + void withClassAndUserSuppliedBackingList() { + doTestWithClass(new AutoPopulatingList<>(new ArrayList<>(), TestObject.class)); } @Test - void withElementFactory() throws Exception { + void withElementFactory() { doTestWithElementFactory(new AutoPopulatingList<>(new MockElementFactory())); } @Test - void withElementFactoryAndUserSuppliedBackingList() throws Exception { + void withElementFactoryAndUserSuppliedBackingList() { doTestWithElementFactory(new AutoPopulatingList<>(new ArrayList<>(), new MockElementFactory())); } @@ -65,7 +66,7 @@ private void doTestWithClass(AutoPopulatingList list) { String helloWorld = "Hello World!"; list.add(10, null); list.add(11, helloWorld); - assertThat(list.get(11)).isEqualTo(helloWorld); + assertThat(list).element(11, InstanceOfAssertFactories.STRING).isEqualTo(helloWorld); boolean condition3 = list.get(10) instanceof TestObject; assertThat(condition3).isTrue(); diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java index d894f91fb8c1..dd834e75dfa9 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import java.util.Set; import java.util.function.Supplier; +import a.ClassHavingNestedClass; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -48,7 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link ClassUtils}. + * Tests for {@link ClassUtils}. * * @author Colin Sampaleanu * @author Juergen Hoeller @@ -86,6 +87,11 @@ void forName() throws ClassNotFoundException { void forNameWithNestedType() throws ClassNotFoundException { assertThat(ClassUtils.forName("org.springframework.util.ClassUtilsTests$NestedClass", classLoader)).isEqualTo(NestedClass.class); assertThat(ClassUtils.forName("org.springframework.util.ClassUtilsTests.NestedClass", classLoader)).isEqualTo(NestedClass.class); + + // Precondition: package name must have length == 1. + assertThat(ClassHavingNestedClass.class.getPackageName().length()).isEqualTo(1); + assertThat(ClassUtils.forName("a.ClassHavingNestedClass$NestedClass", classLoader)).isEqualTo(ClassHavingNestedClass.NestedClass.class); + assertThat(ClassUtils.forName("a.ClassHavingNestedClass.NestedClass", classLoader)).isEqualTo(ClassHavingNestedClass.NestedClass.class); } @Test @@ -391,6 +397,35 @@ void determineCommonAncestor() { assertThat(ClassUtils.determineCommonAncestor(String.class, List.class)).isNull(); } + @Test + void getMostSpecificMethod() throws NoSuchMethodException { + Method defaultPrintMethod = ClassUtils.getMethod(MethodsInterface.class, "defaultPrint"); + assertThat(ClassUtils.getMostSpecificMethod(defaultPrintMethod, MethodsInterfaceImplementation.class)) + .isEqualTo(defaultPrintMethod); + assertThat(ClassUtils.getMostSpecificMethod(defaultPrintMethod, SubMethodsInterfaceImplementation.class)) + .isEqualTo(defaultPrintMethod); + + Method printMethod = ClassUtils.getMethod(MethodsInterface.class, "print", String.class); + assertThat(ClassUtils.getMostSpecificMethod(printMethod, MethodsInterfaceImplementation.class)) + .isNotEqualTo(printMethod); + assertThat(ClassUtils.getMostSpecificMethod(printMethod, MethodsInterfaceImplementation.class)) + .isEqualTo(ClassUtils.getMethod(MethodsInterfaceImplementation.class, "print", String.class)); + assertThat(ClassUtils.getMostSpecificMethod(printMethod, SubMethodsInterfaceImplementation.class)) + .isEqualTo(ClassUtils.getMethod(MethodsInterfaceImplementation.class, "print", String.class)); + + Method protectedPrintMethod = MethodsInterfaceImplementation.class.getDeclaredMethod("protectedPrint"); + assertThat(ClassUtils.getMostSpecificMethod(protectedPrintMethod, MethodsInterfaceImplementation.class)) + .isEqualTo(protectedPrintMethod); + assertThat(ClassUtils.getMostSpecificMethod(protectedPrintMethod, SubMethodsInterfaceImplementation.class)) + .isEqualTo(SubMethodsInterfaceImplementation.class.getDeclaredMethod("protectedPrint")); + + Method packageAccessiblePrintMethod = MethodsInterfaceImplementation.class.getDeclaredMethod("packageAccessiblePrint"); + assertThat(ClassUtils.getMostSpecificMethod(packageAccessiblePrintMethod, MethodsInterfaceImplementation.class)) + .isEqualTo(packageAccessiblePrintMethod); + assertThat(ClassUtils.getMostSpecificMethod(packageAccessiblePrintMethod, SubMethodsInterfaceImplementation.class)) + .isEqualTo(ClassUtils.getMethod(SubMethodsInterfaceImplementation.class, "packageAccessiblePrint")); + } + @ParameterizedTest @WrapperTypes void isPrimitiveWrapper(Class type) { @@ -419,10 +454,11 @@ void isLambda() { } @Test + @SuppressWarnings("Convert2Lambda") void isNotLambda() { assertIsNotLambda(new EnigmaSupplier()); - assertIsNotLambda(new Supplier() { + assertIsNotLambda(new Supplier<>() { @Override public String get() { return "anonymous inner class"; @@ -558,4 +594,46 @@ public String get() { } } + @SuppressWarnings("unused") + private interface MethodsInterface { + + default void defaultPrint() { + + } + + void print(String messages); + } + + @SuppressWarnings("unused") + private class MethodsInterfaceImplementation implements MethodsInterface { + + @Override + public void print(String message) { + + } + + protected void protectedPrint() { + + } + + void packageAccessiblePrint() { + + } + } + + @SuppressWarnings("unused") + private class SubMethodsInterfaceImplementation extends MethodsInterfaceImplementation { + + @Override + protected void protectedPrint() { + + } + + @Override + public void packageAccessiblePrint() { + + } + + } + } diff --git a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java index ac9999ff1fbe..df7f94472cf7 100644 --- a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.util; import java.util.ArrayList; +import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -34,6 +35,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link CollectionUtils}. + * * @author Rob Harrop * @author Juergen Hoeller * @author Rick Evans @@ -63,9 +66,7 @@ void mergeArrayIntoCollection() { list.add("value3"); CollectionUtils.mergeArrayIntoCollection(arr, list); - assertThat(list.get(0)).isEqualTo("value3"); - assertThat(list.get(1)).isEqualTo("value1"); - assertThat(list.get(2)).isEqualTo("value2"); + assertThat(list).containsExactly("value3", "value1", "value2"); } @Test @@ -75,9 +76,7 @@ void mergePrimitiveArrayIntoCollection() { list.add(3); CollectionUtils.mergeArrayIntoCollection(arr, list); - assertThat(list.get(0)).isEqualTo(3); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); + assertThat(list).containsExactly(3, 1, 2); } @Test @@ -115,7 +114,7 @@ void contains() { } @Test - void containsAny() throws Exception { + void containsAny() { List source = new ArrayList<>(); source.add("abc"); source.add("def"); @@ -134,19 +133,19 @@ void containsAny() throws Exception { } @Test - void containsInstanceWithNullCollection() throws Exception { + void containsInstanceWithNullCollection() { assertThat(CollectionUtils.containsInstance(null, this)).as("Must return false if supplied Collection argument is null").isFalse(); } @Test - void containsInstanceWithInstancesThatAreEqualButDistinct() throws Exception { + void containsInstanceWithInstancesThatAreEqualButDistinct() { List list = new ArrayList<>(); list.add(new Instance("fiona")); assertThat(CollectionUtils.containsInstance(list, new Instance("fiona"))).as("Must return false if instance is not in the supplied Collection argument").isFalse(); } @Test - void containsInstanceWithSameInstance() throws Exception { + void containsInstanceWithSameInstance() { List list = new ArrayList<>(); list.add(new Instance("apple")); Instance instance = new Instance("fiona"); @@ -155,7 +154,7 @@ void containsInstanceWithSameInstance() throws Exception { } @Test - void containsInstanceWithNullInstance() throws Exception { + void containsInstanceWithNullInstance() { List list = new ArrayList<>(); list.add(new Instance("apple")); list.add(new Instance("fiona")); @@ -163,7 +162,7 @@ void containsInstanceWithNullInstance() throws Exception { } @Test - void findFirstMatch() throws Exception { + void findFirstMatch() { List source = new ArrayList<>(); source.add("abc"); source.add("def"); @@ -211,6 +210,30 @@ void hasUniqueObject() { assertThat(CollectionUtils.hasUniqueObject(list)).isFalse(); } + @Test + void conversionOfEmptyMap() { + MultiValueMap asMultiValueMap = CollectionUtils.toMultiValueMap(new HashMap<>()); + assertThat(asMultiValueMap).isEmpty(); + assertThat(asMultiValueMap).isEmpty(); + } + + @Test + void conversionOfNonEmptyMap() { + Map> wrapped = new HashMap<>(); + wrapped.put("key", Arrays.asList("first", "second")); + MultiValueMap asMultiValueMap = CollectionUtils.toMultiValueMap(wrapped); + assertThat(asMultiValueMap).containsAllEntriesOf(wrapped); + } + + @Test + void changesValueByReference() { + Map> wrapped = new HashMap<>(); + MultiValueMap asMultiValueMap = CollectionUtils.toMultiValueMap(wrapped); + assertThat(asMultiValueMap).doesNotContainKeys("key"); + wrapped.put("key", new ArrayList<>()); + assertThat(asMultiValueMap).containsKey("key"); + } + private static final class Instance { diff --git a/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java b/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java index 737e062c5e23..d6f19d0a49e6 100644 --- a/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java +++ b/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ void singleIterator() { void multipleIterators() { CompositeIterator it = new CompositeIterator<>(); it.add(Arrays.asList("0", "1").iterator()); - it.add(Arrays.asList("2").iterator()); + it.add(List.of("2").iterator()); it.add(Arrays.asList("3", "4").iterator()); for (int i = 0; i < 5; i++) { assertThat(it.hasNext()).isTrue(); diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java index df1ef39989ac..95a148ae9817 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,7 @@ package org.springframework.util; -import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -27,21 +25,19 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.WeakHashMap; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap.Entry; import org.springframework.util.ConcurrentReferenceHashMap.Reference; import org.springframework.util.ConcurrentReferenceHashMap.Restructure; -import org.springframework.util.comparator.ComparableComparator; -import org.springframework.util.comparator.NullSafeComparator; +import org.springframework.util.comparator.Comparators; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link ConcurrentReferenceHashMap}. @@ -51,8 +47,7 @@ */ class ConcurrentReferenceHashMapTests { - private static final Comparator NULL_SAFE_STRING_SORT = new NullSafeComparator<>( - new ComparableComparator(), true); + private static final Comparator NULL_SAFE_STRING_SORT = Comparators.nullsLow(); private TestWeakConcurrentCache map = new TestWeakConcurrentCache<>(); @@ -101,26 +96,23 @@ void shouldCreateFullyCustom() { @Test void shouldNeedNonNegativeInitialCapacity() { - new ConcurrentReferenceHashMap(0, 1); - assertThatIllegalArgumentException().isThrownBy(() -> - new TestWeakConcurrentCache(-1, 1)) - .withMessageContaining("Initial capacity must not be negative"); + assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap(0, 1)); + assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap(-1, 1)) + .withMessageContaining("Initial capacity must not be negative"); } @Test void shouldNeedPositiveLoadFactor() { - new ConcurrentReferenceHashMap(0, 0.1f, 1); - assertThatIllegalArgumentException().isThrownBy(() -> - new TestWeakConcurrentCache(0, 0.0f, 1)) - .withMessageContaining("Load factor must be positive"); + assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap(0, 0.1f, 1)); + assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap(0, 0.0f, 1)) + .withMessageContaining("Load factor must be positive"); } @Test void shouldNeedPositiveConcurrencyLevel() { - new ConcurrentReferenceHashMap(1, 1); - assertThatIllegalArgumentException().isThrownBy(() -> - new TestWeakConcurrentCache(1, 0)) - .withMessageContaining("Concurrency level must be positive"); + assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap(1, 1)); + assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap(1, 0)) + .withMessageContaining("Concurrency level must be positive"); } @Test @@ -280,7 +272,7 @@ void shouldRemoveKeyAndValue() { assertThat(this.map.get(123)).isEqualTo("123"); assertThat(this.map.remove(123, "123")).isTrue(); assertThat(this.map.containsKey(123)).isFalse(); - assertThat(this.map.isEmpty()).isTrue(); + assertThat(this.map).isEmpty(); } @Test @@ -290,7 +282,7 @@ void shouldRemoveKeyAndValueWithExistingNull() { assertThat(this.map.get(123)).isNull(); assertThat(this.map.remove(123, null)).isTrue(); assertThat(this.map.containsKey(123)).isFalse(); - assertThat(this.map.isEmpty()).isTrue(); + assertThat(this.map).isEmpty(); } @Test @@ -336,11 +328,11 @@ void shouldGetSize() { @Test void shouldSupportIsEmpty() { - assertThat(this.map.isEmpty()).isTrue(); + assertThat(this.map).isEmpty(); this.map.put(123, "123"); this.map.put(123, null); this.map.put(456, "456"); - assertThat(this.map.isEmpty()).isFalse(); + assertThat(this.map).isNotEmpty(); } @Test @@ -371,14 +363,14 @@ void shouldRemoveWhenKeyIsInMap() { assertThat(this.map.remove(123)).isNull(); assertThat(this.map.remove(456)).isEqualTo("456"); assertThat(this.map.remove(null)).isEqualTo("789"); - assertThat(this.map.isEmpty()).isTrue(); + assertThat(this.map).isEmpty(); } @Test void shouldRemoveWhenKeyIsNotInMap() { assertThat(this.map.remove(123)).isNull(); assertThat(this.map.remove(null)).isNull(); - assertThat(this.map.isEmpty()).isTrue(); + assertThat(this.map).isEmpty(); } @Test @@ -496,32 +488,17 @@ void containsViaEntrySet() { this.map.put(3, "3"); Set> entrySet = this.map.entrySet(); Set> copy = new HashMap<>(this.map).entrySet(); - copy.forEach(entry -> assertThat(entrySet.contains(entry)).isTrue()); + copy.forEach(entry -> assertThat(entrySet).contains(entry)); this.map.put(1, "A"); this.map.put(2, "B"); this.map.put(3, "C"); - copy.forEach(entry -> assertThat(entrySet.contains(entry)).isFalse()); + copy.forEach(entry -> assertThat(entrySet).doesNotContain(entry)); this.map.put(1, "1"); this.map.put(2, "2"); this.map.put(3, "3"); - copy.forEach(entry -> assertThat(entrySet.contains(entry)).isTrue()); + copy.forEach(entry -> assertThat(entrySet).contains(entry)); entrySet.clear(); - copy.forEach(entry -> assertThat(entrySet.contains(entry)).isFalse()); - } - - @Test - @Disabled("Intended for use during development only") - void shouldBeFasterThanSynchronizedMap() throws InterruptedException { - Map> synchronizedMap = Collections.synchronizedMap(new WeakHashMap>()); - StopWatch mapTime = timeMultiThreaded("SynchronizedMap", synchronizedMap, v -> new WeakReference<>(String.valueOf(v))); - System.out.println(mapTime.prettyPrint()); - - this.map.setDisableTestHooks(true); - StopWatch cacheTime = timeMultiThreaded("WeakConcurrentCache", this.map, String::valueOf); - System.out.println(cacheTime.prettyPrint()); - - // We should be at least 4 time faster - assertThat(cacheTime.getTotalTimeSeconds()).isLessThan(mapTime.getTotalTimeSeconds() / 4.0); + copy.forEach(entry -> assertThat(entrySet).doesNotContain(entry)); } @Test @@ -530,50 +507,6 @@ void shouldSupportNullReference() { map.createReferenceManager().createReference(null, 1234, null); } - /** - * Time a multi-threaded access to a cache. - * @return the timing stopwatch - */ - private StopWatch timeMultiThreaded(String id, final Map map, - ValueFactory factory) throws InterruptedException { - - StopWatch stopWatch = new StopWatch(id); - for (int i = 0; i < 500; i++) { - map.put(i, factory.newValue(i)); - } - Thread[] threads = new Thread[30]; - stopWatch.start("Running threads"); - for (int threadIndex = 0; threadIndex < threads.length; threadIndex++) { - threads[threadIndex] = new Thread("Cache access thread " + threadIndex) { - @Override - public void run() { - for (int j = 0; j < 1000; j++) { - for (int i = 0; i < 1000; i++) { - map.get(i); - } - } - } - }; - } - for (Thread thread : threads) { - thread.start(); - } - - for (Thread thread : threads) { - if (thread.isAlive()) { - thread.join(2000); - } - } - stopWatch.stop(); - return stopWatch; - } - - - private interface ValueFactory { - - V newValue(int k); - } - private static class TestWeakConcurrentCache extends ConcurrentReferenceHashMap { @@ -581,29 +514,16 @@ private static class TestWeakConcurrentCache extends ConcurrentReferenceHa private final LinkedList> queue = new LinkedList<>(); - private boolean disableTestHooks; - public TestWeakConcurrentCache() { super(); } - public void setDisableTestHooks(boolean disableTestHooks) { - this.disableTestHooks = disableTestHooks; - } - public TestWeakConcurrentCache(int initialCapacity, float loadFactor, int concurrencyLevel) { super(initialCapacity, loadFactor, concurrencyLevel); } - public TestWeakConcurrentCache(int initialCapacity, int concurrencyLevel) { - super(initialCapacity, concurrencyLevel); - } - @Override protected int getHash(@Nullable Object o) { - if (this.disableTestHooks) { - return super.getHash(o); - } // For testing we want more control of the hash this.supplementalHash = super.getHash(o); return (o != null ? o.hashCode() : 0); @@ -618,16 +538,10 @@ protected ReferenceManager createReferenceManager() { return new ReferenceManager() { @Override public Reference createReference(Entry entry, int hash, @Nullable Reference next) { - if (TestWeakConcurrentCache.this.disableTestHooks) { - return super.createReference(entry, hash, next); - } return new MockReference<>(entry, hash, next, TestWeakConcurrentCache.this.queue); } @Override public Reference pollForPurge() { - if (TestWeakConcurrentCache.this.disableTestHooks) { - return super.pollForPurge(); - } return TestWeakConcurrentCache.this.queue.isEmpty() ? null : TestWeakConcurrentCache.this.queue.removeFirst(); } }; diff --git a/spring-core/src/test/java/org/springframework/util/DigestUtilsTests.java b/spring-core/src/test/java/org/springframework/util/DigestUtilsTests.java index 2ba9c6724017..bad71408ac05 100644 --- a/spring-core/src/test/java/org/springframework/util/DigestUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/DigestUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,7 +37,7 @@ class DigestUtilsTests { @BeforeEach void createBytes() throws UnsupportedEncodingException { - bytes = "Hello World".getBytes("UTF-8"); + bytes = "Hello World".getBytes(StandardCharsets.UTF_8); } diff --git a/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java b/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java index b4d4d414ef1c..2b7a4deb15fc 100644 --- a/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java +++ b/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,10 @@ package org.springframework.util; +import java.util.List; + import org.junit.jupiter.api.Test; -import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; /** @@ -28,7 +29,7 @@ class ExceptionTypeFilterTests { @Test void subClassMatch() { - ExceptionTypeFilter filter = new ExceptionTypeFilter(asList(RuntimeException.class), null, true); + ExceptionTypeFilter filter = new ExceptionTypeFilter(List.of(RuntimeException.class), null, true); assertThat(filter.match(RuntimeException.class)).isTrue(); assertThat(filter.match(IllegalStateException.class)).isTrue(); } diff --git a/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java b/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java index 8080f82fe0ae..71e5c04ef756 100644 --- a/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java +++ b/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.util; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + import org.junit.jupiter.api.Test; import org.springframework.util.backoff.BackOffExecution; @@ -25,6 +29,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** + * Tests for {@link ExponentialBackOff}. + * * @author Stephane Nicoll */ class ExponentialBackOffTests { @@ -121,14 +127,27 @@ void maxIntervalReachedImmediately() { } @Test - void toStringContent() { + void executionToStringContent() { ExponentialBackOff backOff = new ExponentialBackOff(2000L, 2.0); BackOffExecution execution = backOff.start(); - assertThat(execution.toString()).isEqualTo("ExponentialBackOff{currentInterval=n/a, multiplier=2.0}"); + assertThat(execution.toString()).isEqualTo("ExponentialBackOffExecution{currentInterval=n/a, multiplier=2.0, attempts=0}"); execution.nextBackOff(); - assertThat(execution.toString()).isEqualTo("ExponentialBackOff{currentInterval=2000ms, multiplier=2.0}"); + assertThat(execution.toString()).isEqualTo("ExponentialBackOffExecution{currentInterval=2000ms, multiplier=2.0, attempts=1}"); execution.nextBackOff(); - assertThat(execution.toString()).isEqualTo("ExponentialBackOff{currentInterval=4000ms, multiplier=2.0}"); + assertThat(execution.toString()).isEqualTo("ExponentialBackOffExecution{currentInterval=4000ms, multiplier=2.0, attempts=2}"); + } + + @Test + void maxAttempts() { + ExponentialBackOff backOff = new ExponentialBackOff(); + backOff.setInitialInterval(1000L); + backOff.setMultiplier(2.0); + backOff.setMaxInterval(10000L); + backOff.setMaxAttempts(6); + List delays = new ArrayList<>(); + BackOffExecution execution = backOff.start(); + IntStream.range(0, 7).forEach(i -> delays.add(execution.nextBackOff())); + assertThat(delays).containsExactly(1000L, 2000L, 4000L, 8000L, 10000L, 10000L, BackOffExecution.STOP); } } diff --git a/spring-core/src/test/java/org/springframework/util/FastByteArrayOutputStreamTests.java b/spring-core/src/test/java/org/springframework/util/FastByteArrayOutputStreamTests.java index 675810fe96e3..c5e6a14d532b 100644 --- a/spring-core/src/test/java/org/springframework/util/FastByteArrayOutputStreamTests.java +++ b/spring-core/src/test/java/org/springframework/util/FastByteArrayOutputStreamTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,15 +29,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Test suite for {@link FastByteArrayOutputStream}. + * Tests for {@link FastByteArrayOutputStream}. * * @author Craig Andrews */ class FastByteArrayOutputStreamTests { - private static final int INITIAL_CAPACITY = 256; - - private final FastByteArrayOutputStream os = new FastByteArrayOutputStream(INITIAL_CAPACITY); + private final FastByteArrayOutputStream os = new FastByteArrayOutputStream(); private final byte[] helloBytes = "Hello World".getBytes(StandardCharsets.UTF_8); @@ -57,6 +55,26 @@ void resize() throws Exception { assertThat(this.os.size()).isEqualTo(sizeBefore); } + @Test + void stringConversion() throws Exception { + this.os.write(this.helloBytes); + assertThat(this.os.toString()).isEqualTo("Hello World"); + assertThat(this.os.toString(StandardCharsets.UTF_8)).isEqualTo("Hello World"); + + @SuppressWarnings("resource") + FastByteArrayOutputStream empty = new FastByteArrayOutputStream(); + assertThat(empty.toString()).isEqualTo(""); + assertThat(empty.toString(StandardCharsets.US_ASCII)).isEqualTo(""); + + @SuppressWarnings("resource") + FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream(5); + // Add bytes in multiple writes to ensure we get more than one buffer internally + outputStream.write(this.helloBytes, 0, 5); + outputStream.write(this.helloBytes, 5, 6); + assertThat(outputStream.toString()).isEqualTo("Hello World"); + assertThat(outputStream.toString(StandardCharsets.UTF_8)).isEqualTo("Hello World"); + } + @Test void autoGrow() throws IOException { this.os.resize(1); @@ -84,10 +102,9 @@ void reset() throws Exception { } @Test - void close() throws Exception { + void close() { this.os.close(); - assertThatIOException().isThrownBy(() -> - this.os.write(this.helloBytes)); + assertThatIOException().isThrownBy(() -> this.os.write(this.helloBytes)); } @Test @@ -110,8 +127,9 @@ void writeTo() throws Exception { @Test void failResize() throws Exception { this.os.write(this.helloBytes); - assertThatIllegalArgumentException().isThrownBy(() -> - this.os.resize(5)); + assertThatIllegalArgumentException() + .isThrownBy(() -> this.os.resize(5)) + .withMessage("New capacity must not be smaller than current size"); } @Test @@ -138,7 +156,7 @@ void getInputStreamRead() throws Exception { @Test void getInputStreamReadBytePromotion() throws Exception { - byte[] bytes = new byte[] { -1 }; + byte[] bytes = { -1 }; this.os.write(bytes); InputStream inputStream = this.os.getInputStream(); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); diff --git a/spring-core/src/test/java/org/springframework/util/FileCopyUtilsTests.java b/spring-core/src/test/java/org/springframework/util/FileCopyUtilsTests.java index e0958a605157..2d1886724d27 100644 --- a/spring-core/src/test/java/org/springframework/util/FileCopyUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/FileCopyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for the FileCopyUtils class. + * Tests for {@link FileCopyUtils}. * * @author Juergen Hoeller * @since 12.03.2005 diff --git a/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java b/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java index e89c999055e9..c121d1a3375c 100644 --- a/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java @@ -80,7 +80,7 @@ void copyRecursively() throws Exception { @AfterEach - void tearDown() throws Exception { + void tearDown() { File tmp = new File("./tmp"); if (tmp.exists()) { FileSystemUtils.deleteRecursively(tmp); diff --git a/spring-core/src/test/java/org/springframework/util/InstanceFilterTests.java b/spring-core/src/test/java/org/springframework/util/InstanceFilterTests.java index 8e761b369351..9c2bf39b4cd4 100644 --- a/spring-core/src/test/java/org/springframework/util/InstanceFilterTests.java +++ b/spring-core/src/test/java/org/springframework/util/InstanceFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.util; +import java.util.List; + import org.junit.jupiter.api.Test; import static java.util.Arrays.asList; @@ -60,7 +62,7 @@ void includesAndExcludesFilters() { @Test void includesAndExcludesFiltersConflict() { InstanceFilter filter = new InstanceFilter<>( - asList("First"), asList("First"), true); + List.of("First"), List.of("First"), true); doNotMatch(filter, "First"); } diff --git a/spring-core/src/test/java/org/springframework/util/MethodInvokerTests.java b/spring-core/src/test/java/org/springframework/util/MethodInvokerTests.java index 13d265d57075..c0e20c1c8cad 100644 --- a/spring-core/src/test/java/org/springframework/util/MethodInvokerTests.java +++ b/spring-core/src/test/java/org/springframework/util/MethodInvokerTests.java @@ -80,7 +80,7 @@ void plainMethodInvoker() throws Exception { } @Test - void stringWithMethodInvoker() throws Exception { + void stringWithMethodInvoker() { MethodInvoker methodInvoker = new MethodInvoker(); methodInvoker.setTargetObject(new Greeter()); methodInvoker.setTargetMethod("greet"); diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java index e582bc60ec40..4bb95755256f 100644 --- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link MimeType}. + * Tests for {@link MimeType}. * * @author Arjen Poutsma * @author Juergen Hoeller diff --git a/spring-core/src/test/java/org/springframework/util/MultiValueMapTests.java b/spring-core/src/test/java/org/springframework/util/MultiValueMapTests.java new file mode 100644 index 000000000000..4dcd4f302ff1 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/MultiValueMapTests.java @@ -0,0 +1,202 @@ +/* + * 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.util; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * Tests for {@link MultiValueMap}. + * + * @author Mihai Dumitrescu + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Sam Brannen + */ +class MultiValueMapTests { + + @ParameterizedMultiValueMapTest + void add(MultiValueMap map) { + int initialSize = map.size(); + map.add("key", "value1"); + map.add("key", "value2"); + assertThat(map).hasSize(initialSize + 1); + assertThat(map.get("key")).containsExactly("value1", "value2"); + } + + @ParameterizedMultiValueMapTest + void addIfAbsentWhenAbsent(MultiValueMap map) { + map.addIfAbsent("key", "value1"); + assertThat(map.get("key")).containsExactly("value1"); + } + + @ParameterizedMultiValueMapTest + void addIfAbsentWhenPresent(MultiValueMap map) { + map.add("key", "value1"); + map.addIfAbsent("key", "value2"); + assertThat(map.get("key")).containsExactly("value1"); + } + + @ParameterizedMultiValueMapTest + void set(MultiValueMap map) { + map.set("key", "value1"); + map.set("key", "value2"); + assertThat(map.get("key")).containsExactly("value2"); + } + + @ParameterizedMultiValueMapTest + void addAll(MultiValueMap map) { + int initialSize = map.size(); + map.add("key", "value1"); + map.addAll("key", Arrays.asList("value2", "value3")); + assertThat(map).hasSize(initialSize + 1); + assertThat(map.get("key")).containsExactly("value1", "value2", "value3"); + } + + @ParameterizedMultiValueMapTest + void addAllWithEmptyList(MultiValueMap map) { + int initialSize = map.size(); + map.addAll("key", List.of()); + assertThat(map).hasSize(initialSize + 1); + assertThat(map.get("key")).isEmpty(); + assertThat(map.getFirst("key")).isNull(); + } + + @ParameterizedMultiValueMapTest + void getFirst(MultiValueMap map) { + List values = List.of("value1", "value2"); + map.put("key", values); + assertThat(map.getFirst("key")).isEqualTo("value1"); + assertThat(map.getFirst("other")).isNull(); + } + + @ParameterizedMultiValueMapTest + void toSingleValueMap(MultiValueMap map) { + int initialSize = map.size(); + List values = List.of("value1", "value2"); + map.put("key", values); + Map singleValueMap = map.toSingleValueMap(); + assertThat(singleValueMap).hasSize(initialSize + 1); + assertThat(singleValueMap.get("key")).isEqualTo("value1"); + } + + @ParameterizedMultiValueMapTest + void toSingleValueMapWithEmptyList(MultiValueMap map) { + int initialSize = map.size(); + map.put("key", List.of()); + Map singleValueMap = map.toSingleValueMap(); + assertThat(singleValueMap).hasSize(initialSize); + assertThat(singleValueMap.get("key")).isNull(); + } + + @ParameterizedMultiValueMapTest + void equalsOnExistingValues(MultiValueMap map) { + map.clear(); + map.set("key1", "value1"); + assertThat(map).isEqualTo(map); + } + + @ParameterizedMultiValueMapTest + void equalsOnEmpty(MultiValueMap map) { + map.clear(); + map.set("key1", "value1"); + MultiValueMap map1 = new LinkedMultiValueMap<>(); + map1.set("key1", "value1"); + assertThat(map1).isEqualTo(map); + assertThat(map).isEqualTo(map1); + Map> map2 = Map.of("key1", List.of("value1")); + assertThat(map2).isEqualTo(map); + assertThat(map).isEqualTo(map2); + } + + @Test + void canNotChangeAnUnmodifiableMultiValueMap() { + MultiValueMap map = new LinkedMultiValueMap<>(); + MultiValueMap unmodifiableMap = CollectionUtils.unmodifiableMultiValueMap(map); + assertSoftly(softly -> { + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.add("key", "value")); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.addIfAbsent("key", "value")); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.addAll("key", exampleListOfValues())); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.addAll(exampleMultiValueMap())); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.set("key", "value")); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.setAll(exampleHashMap())); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.put("key", exampleListOfValues())); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.putIfAbsent("key", exampleListOfValues())); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.putAll(exampleMultiValueMap())); + softly.assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> unmodifiableMap.remove("key1")); + }); + } + + private static List exampleListOfValues() { + return List.of("value1", "value2"); + } + + private static Map exampleHashMap() { + return Map.of("key2", "key2.value1"); + } + + private static MultiValueMap exampleMultiValueMap() { + LinkedMultiValueMap map = new LinkedMultiValueMap<>(); + map.put("key1", Arrays.asList("key1.value1", "key1.value2")); + return map; + } + + + @Retention(RetentionPolicy.RUNTIME) + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("mapsUnderTest") + @interface ParameterizedMultiValueMapTest { + } + + static Stream mapsUnderTest() { + return Stream.of( + arguments(named("new LinkedMultiValueMap<>()", new LinkedMultiValueMap<>())), + arguments(named("new LinkedMultiValueMap<>(new HashMap<>())", new LinkedMultiValueMap<>(new HashMap<>()))), + arguments(named("new LinkedMultiValueMap<>(new LinkedHashMap<>())", new LinkedMultiValueMap<>(new LinkedHashMap<>()))), + arguments(named("new LinkedMultiValueMap<>(Map.of(...))", new LinkedMultiValueMap<>(Map.of("existingkey", List.of("existingvalue1", "existingvalue2"))))), + arguments(named("CollectionUtils.toMultiValueMap", CollectionUtils.toMultiValueMap(new HashMap<>()))) + ); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/NumberUtilsTests.java b/spring-core/src/test/java/org/springframework/util/NumberUtilsTests.java index 5c07daf8d062..714e9de0085a 100644 --- a/spring-core/src/test/java/org/springframework/util/NumberUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/NumberUtilsTests.java @@ -104,8 +104,8 @@ void parseNumberRequiringTrimUsingNumberFormat() { @Test void parseNumberAsHex() { - String aByte = "0x" + Integer.toHexString(Byte.valueOf(Byte.MAX_VALUE)); - String aShort = "0x" + Integer.toHexString(Short.valueOf(Short.MAX_VALUE)); + String aByte = "0x" + Integer.toHexString(Byte.MAX_VALUE); + String aShort = "0x" + Integer.toHexString(Short.MAX_VALUE); String anInteger = "0x" + Integer.toHexString(Integer.MAX_VALUE); String aLong = "0x" + Long.toHexString(Long.MAX_VALUE); String aReallyBigInt = "FEBD4E677898DFEBFFEE44"; @@ -283,7 +283,7 @@ void convertToInteger() { assertThat(NumberUtils.convertNumberToTargetClass((long) -1, Integer.class)).isEqualTo(Integer.valueOf(-1)); assertThat(NumberUtils.convertNumberToTargetClass(0L, Integer.class)).isEqualTo(Integer.valueOf(0)); assertThat(NumberUtils.convertNumberToTargetClass(1L, Integer.class)).isEqualTo(Integer.valueOf(1)); - assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Integer.MAX_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass((long) Integer.MAX_VALUE, Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); assertThat(NumberUtils.convertNumberToTargetClass((long) (Integer.MAX_VALUE + 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); assertThat(NumberUtils.convertNumberToTargetClass((long) Integer.MIN_VALUE, Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); assertThat(NumberUtils.convertNumberToTargetClass((long) (Integer.MIN_VALUE - 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); diff --git a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java index 8090c4be82f0..f7107de888fd 100644 --- a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.sql.SQLException; import java.time.LocalDate; import java.time.ZoneId; +import java.util.Arrays; import java.util.Collections; import java.util.Currency; import java.util.Date; @@ -38,6 +39,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TimeZone; @@ -53,7 +55,7 @@ import static org.springframework.util.ObjectUtils.isEmpty; /** - * Unit tests for {@link ObjectUtils}. + * Tests for {@link ObjectUtils}. * * @author Rod Johnson * @author Juergen Hoeller @@ -379,193 +381,221 @@ void isPrimitiveOrWrapperWithShortWrapperClass() { } @Test - void nullSafeHashCodeWithBooleanArray() { - int expected = 31 * 7 + Boolean.TRUE.hashCode(); - expected = 31 * expected + Boolean.FALSE.hashCode(); + void nullSafeHashWithNull() { + assertThat(ObjectUtils.nullSafeHash((Object[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashWithIntermediateNullElements() { + assertThat(ObjectUtils.nullSafeHash(3, null, 5)).isEqualTo(Objects.hash(3, null, 5)); + } + @Test + @Deprecated + void nullSafeHashCodeWithNullBooleanArray() { + boolean[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); + } + + @Test + @Deprecated + void nullSafeHashCodeWithBooleanArray() { boolean[] array = {true, false}; int actual = ObjectUtils.nullSafeHashCode(array); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); + } - assertThat(actual).isEqualTo(expected); + @Test + @Deprecated + void nullSafeHashCodeWithObjectBeingBooleanArray() { + Object array = new boolean[] {true, false}; + int expected = ObjectUtils.nullSafeHashCode((boolean[]) array); + assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithBooleanArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((boolean[]) null)).isEqualTo(0); + @Deprecated + void nullSafeHashCodeWithNullByteArray() { + byte[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test + @Deprecated void nullSafeHashCodeWithByteArray() { - int expected = 31 * 7 + 8; - expected = 31 * expected + 10; - byte[] array = {8, 10}; int actual = ObjectUtils.nullSafeHashCode(array); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); + } - assertThat(actual).isEqualTo(expected); + @Test + @Deprecated + void nullSafeHashCodeWithObjectBeingByteArray() { + Object array = new byte[] {6, 39}; + int expected = ObjectUtils.nullSafeHashCode((byte[]) array); + assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithByteArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((byte[]) null)).isEqualTo(0); + @Deprecated + void nullSafeHashCodeWithNullCharArray() { + char[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test + @Deprecated void nullSafeHashCodeWithCharArray() { - int expected = 31 * 7 + 'a'; - expected = 31 * expected + 'E'; - char[] array = {'a', 'E'}; int actual = ObjectUtils.nullSafeHashCode(array); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); + } - assertThat(actual).isEqualTo(expected); + @Test + @Deprecated + void nullSafeHashCodeWithObjectBeingCharArray() { + Object array = new char[] {'l', 'M'}; + int expected = ObjectUtils.nullSafeHashCode((char[]) array); + assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithCharArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((char[]) null)).isEqualTo(0); + @Deprecated + void nullSafeHashCodeWithNullDoubleArray() { + double[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test + @Deprecated void nullSafeHashCodeWithDoubleArray() { - long bits = Double.doubleToLongBits(8449.65); - int expected = 31 * 7 + (int) (bits ^ (bits >>> 32)); - bits = Double.doubleToLongBits(9944.923); - expected = 31 * expected + (int) (bits ^ (bits >>> 32)); - double[] array = {8449.65, 9944.923}; int actual = ObjectUtils.nullSafeHashCode(array); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); + } - assertThat(actual).isEqualTo(expected); + @Test + @Deprecated + void nullSafeHashCodeWithObjectBeingDoubleArray() { + Object array = new double[] {68930.993, 9022.009}; + int expected = ObjectUtils.nullSafeHashCode((double[]) array); + assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithDoubleArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((double[]) null)).isEqualTo(0); + @Deprecated + void nullSafeHashCodeWithNullFloatArray() { + float[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test + @Deprecated void nullSafeHashCodeWithFloatArray() { - int expected = 31 * 7 + Float.floatToIntBits(9.6f); - expected = 31 * expected + Float.floatToIntBits(7.4f); - float[] array = {9.6f, 7.4f}; int actual = ObjectUtils.nullSafeHashCode(array); - - assertThat(actual).isEqualTo(expected); - } - - @Test - void nullSafeHashCodeWithFloatArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((float[]) null)).isEqualTo(0); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); } @Test - void nullSafeHashCodeWithIntArray() { - int expected = 31 * 7 + 884; - expected = 31 * expected + 340; - - int[] array = {884, 340}; - int actual = ObjectUtils.nullSafeHashCode(array); - - assertThat(actual).isEqualTo(expected); + @Deprecated + void nullSafeHashCodeWithObjectBeingFloatArray() { + Object array = new float[] {9.9f, 9.54f}; + int expected = ObjectUtils.nullSafeHashCode((float[]) array); + assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithIntArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((int[]) null)).isEqualTo(0); + @Deprecated + void nullSafeHashCodeWithNullIntArray() { + int[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test - void nullSafeHashCodeWithLongArray() { - long lng = 7993L; - int expected = 31 * 7 + (int) (lng ^ (lng >>> 32)); - lng = 84320L; - expected = 31 * expected + (int) (lng ^ (lng >>> 32)); - - long[] array = {7993L, 84320L}; + @Deprecated + void nullSafeHashCodeWithIntArray() { + int[] array = {884, 340}; int actual = ObjectUtils.nullSafeHashCode(array); - - assertThat(actual).isEqualTo(expected); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); } @Test - void nullSafeHashCodeWithLongArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((long[]) null)).isEqualTo(0); + @Deprecated + void nullSafeHashCodeWithObjectBeingIntArray() { + Object array = new int[] {89, 32}; + int expected = ObjectUtils.nullSafeHashCode((int[]) array); + assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithObject() { - String str = "Luke"; - assertThat(ObjectUtils.nullSafeHashCode(str)).isEqualTo(str.hashCode()); + @Deprecated + void nullSafeHashCodeWithNullLongArray() { + long[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test - void nullSafeHashCodeWithObjectArray() { - int expected = 31 * 7 + "Leia".hashCode(); - expected = 31 * expected + "Han".hashCode(); - - Object[] array = {"Leia", "Han"}; + @Deprecated + void nullSafeHashCodeWithLongArray() { + long[] array = {7993L, 84320L}; int actual = ObjectUtils.nullSafeHashCode(array); - - assertThat(actual).isEqualTo(expected); - } - - @Test - void nullSafeHashCodeWithObjectArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((Object[]) null)).isEqualTo(0); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); } @Test - void nullSafeHashCodeWithObjectBeingBooleanArray() { - Object array = new boolean[] {true, false}; - int expected = ObjectUtils.nullSafeHashCode((boolean[]) array); + @Deprecated + void nullSafeHashCodeWithObjectBeingLongArray() { + Object array = new long[] {4389, 320}; + int expected = ObjectUtils.nullSafeHashCode((long[]) array); assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithObjectBeingByteArray() { - Object array = new byte[] {6, 39}; - int expected = ObjectUtils.nullSafeHashCode((byte[]) array); - assertEqualHashCodes(expected, array); + @Deprecated + void nullSafeHashCodeWithNullShortArray() { + short[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test - void nullSafeHashCodeWithObjectBeingCharArray() { - Object array = new char[] {'l', 'M'}; - int expected = ObjectUtils.nullSafeHashCode((char[]) array); - assertEqualHashCodes(expected, array); + @Deprecated + void nullSafeHashCodeWithShortArray() { + short[] array = {4, 25}; + int actual = ObjectUtils.nullSafeHashCode(array); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); } @Test - void nullSafeHashCodeWithObjectBeingDoubleArray() { - Object array = new double[] {68930.993, 9022.009}; - int expected = ObjectUtils.nullSafeHashCode((double[]) array); + @Deprecated + void nullSafeHashCodeWithObjectBeingShortArray() { + Object array = new short[] {5, 3}; + int expected = ObjectUtils.nullSafeHashCode((short[]) array); assertEqualHashCodes(expected, array); } @Test - void nullSafeHashCodeWithObjectBeingFloatArray() { - Object array = new float[] {9.9f, 9.54f}; - int expected = ObjectUtils.nullSafeHashCode((float[]) array); - assertEqualHashCodes(expected, array); + void nullSafeHashCodeWithObject() { + String str = "Luke"; + assertThat(ObjectUtils.nullSafeHashCode(str)).isEqualTo(str.hashCode()); } @Test - void nullSafeHashCodeWithObjectBeingIntArray() { - Object array = new int[] {89, 32}; - int expected = ObjectUtils.nullSafeHashCode((int[]) array); - assertEqualHashCodes(expected, array); + @Deprecated + void nullSafeHashCodeWithObjectArray() { + Object[] array = {"Leia", "Han"}; + int actual = ObjectUtils.nullSafeHashCode(array); + assertThat(actual).isEqualTo(Arrays.hashCode(array)); } @Test - void nullSafeHashCodeWithObjectBeingLongArray() { - Object array = new long[] {4389, 320}; - int expected = ObjectUtils.nullSafeHashCode((long[]) array); - assertEqualHashCodes(expected, array); + @Deprecated + void nullSafeHashCodeWithObjectArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((Object[]) null)).isEqualTo(0); } @Test + @Deprecated void nullSafeHashCodeWithObjectBeingObjectArray() { Object array = new Object[] {"Luke", "Anakin"}; int expected = ObjectUtils.nullSafeHashCode((Object[]) array); @@ -573,31 +603,10 @@ void nullSafeHashCodeWithObjectBeingObjectArray() { } @Test - void nullSafeHashCodeWithObjectBeingShortArray() { - Object array = new short[] {5, 3}; - int expected = ObjectUtils.nullSafeHashCode((short[]) array); - assertEqualHashCodes(expected, array); - } - - @Test + @Deprecated void nullSafeHashCodeWithObjectEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((Object) null)).isEqualTo(0); - } - - @Test - void nullSafeHashCodeWithShortArray() { - int expected = 31 * 7 + 70; - expected = 31 * expected + 8; - - short[] array = {70, 8}; - int actual = ObjectUtils.nullSafeHashCode(array); - - assertThat(actual).isEqualTo(expected); - } - - @Test - void nullSafeHashCodeWithShortArrayEqualToNull() { - assertThat(ObjectUtils.nullSafeHashCode((short[]) null)).isEqualTo(0); + Object[] array = null; + assertThat(ObjectUtils.nullSafeHashCode(array)).isEqualTo(0); } @Test diff --git a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java index a96f21b0d3f1..b4618c090d78 100644 --- a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,86 +21,119 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link PatternMatchUtils}. + * * @author Juergen Hoeller * @author Johan Gorter + * @author Sam Brannen */ class PatternMatchUtilsTests { + @Test + void nullAndEmptyValues() { + assertDoesNotMatch((String) null, null); + assertDoesNotMatch((String) null, ""); + assertDoesNotMatch("123", null); + + assertDoesNotMatch((String[]) null, null); + assertDoesNotMatch((String[]) null, ""); + assertDoesNotMatch(new String[] {}, null); + } + @Test void trivial() { - assertThat(PatternMatchUtils.simpleMatch((String) null, "")).isFalse(); - assertThat(PatternMatchUtils.simpleMatch("1", null)).isFalse(); - doTest("*", "123", true); - doTest("123", "123", true); + assertMatches("", ""); + assertMatches("123", "123"); + assertMatches("*", "123"); + + assertMatches(new String[] { "" }, ""); + assertMatches(new String[] { "123" }, "123"); + assertMatches(new String[] { "*" }, "123"); + + assertMatches(new String[] { null, "" }, ""); + assertMatches(new String[] { null, "123" }, "123"); + assertMatches(new String[] { null, "*" }, "123"); } @Test void startsWith() { - doTest("get*", "getMe", true); - doTest("get*", "setMe", false); + assertMatches("get*", "getMe"); + assertDoesNotMatch("get*", "setMe"); } @Test void endsWith() { - doTest("*Test", "getMeTest", true); - doTest("*Test", "setMe", false); + assertMatches("*Test", "getMeTest"); + assertDoesNotMatch("*Test", "setMe"); } @Test void between() { - doTest("*stuff*", "getMeTest", false); - doTest("*stuff*", "getstuffTest", true); - doTest("*stuff*", "stuffTest", true); - doTest("*stuff*", "getstuff", true); - doTest("*stuff*", "stuff", true); + assertDoesNotMatch("*stuff*", "getMeTest"); + assertMatches("*stuff*", "getstuffTest"); + assertMatches("*stuff*", "stuffTest"); + assertMatches("*stuff*", "getstuff"); + assertMatches("*stuff*", "stuff"); } @Test void startsEnds() { - doTest("on*Event", "onMyEvent", true); - doTest("on*Event", "onEvent", true); - doTest("3*3", "3", false); - doTest("3*3", "33", true); + assertMatches("on*Event", "onMyEvent"); + assertMatches("on*Event", "onEvent"); + assertDoesNotMatch("3*3", "3"); + assertMatches("3*3", "33"); } @Test void startsEndsBetween() { - doTest("12*45*78", "12345678", true); - doTest("12*45*78", "123456789", false); - doTest("12*45*78", "012345678", false); - doTest("12*45*78", "124578", true); - doTest("12*45*78", "1245457878", true); - doTest("3*3*3", "33", false); - doTest("3*3*3", "333", true); + assertMatches("12*45*78", "12345678"); + assertDoesNotMatch("12*45*78", "123456789"); + assertDoesNotMatch("12*45*78", "012345678"); + assertMatches("12*45*78", "124578"); + assertMatches("12*45*78", "1245457878"); + assertDoesNotMatch("3*3*3", "33"); + assertMatches("3*3*3", "333"); } @Test void ridiculous() { - doTest("*1*2*3*", "0011002001010030020201030", true); - doTest("1*2*3*4", "10300204", false); - doTest("1*2*3*3", "10300203", false); - doTest("*1*2*3*", "123", true); - doTest("*1*2*3*", "132", false); + assertMatches("*1*2*3*", "0011002001010030020201030"); + assertDoesNotMatch("1*2*3*4", "10300204"); + assertDoesNotMatch("1*2*3*3", "10300203"); + assertMatches("*1*2*3*", "123"); + assertDoesNotMatch("*1*2*3*", "132"); } @Test void patternVariants() { - doTest("*a", "*", false); - doTest("*a", "a", true); - doTest("*a", "b", false); - doTest("*a", "aa", true); - doTest("*a", "ba", true); - doTest("*a", "ab", false); - doTest("**a", "*", false); - doTest("**a", "a", true); - doTest("**a", "b", false); - doTest("**a", "aa", true); - doTest("**a", "ba", true); - doTest("**a", "ab", false); + assertDoesNotMatch("*a", "*"); + assertMatches("*a", "a"); + assertDoesNotMatch("*a", "b"); + assertMatches("*a", "aa"); + assertMatches("*a", "ba"); + assertDoesNotMatch("*a", "ab"); + assertDoesNotMatch("**a", "*"); + assertMatches("**a", "a"); + assertDoesNotMatch("**a", "b"); + assertMatches("**a", "aa"); + assertMatches("**a", "ba"); + assertDoesNotMatch("**a", "ab"); + } + + private void assertMatches(String pattern, String str) { + assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isTrue(); + } + + private void assertDoesNotMatch(String pattern, String str) { + assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse(); + } + + private void assertMatches(String[] patterns, String str) { + assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isTrue(); } - private void doTest(String pattern, String str, boolean shouldMatch) { - assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isEqualTo(shouldMatch); + private void assertDoesNotMatch(String[] patterns, String str) { + assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isFalse(); } } diff --git a/spring-core/src/test/java/org/springframework/util/PropertiesPersisterTests.java b/spring-core/src/test/java/org/springframework/util/PropertiesPersisterTests.java index 0368c892a6d3..7146a4911086 100644 --- a/spring-core/src/test/java/org/springframework/util/PropertiesPersisterTests.java +++ b/spring-core/src/test/java/org/springframework/util/PropertiesPersisterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -113,7 +113,7 @@ private Properties loadProperties(String propString, boolean useReader) throws I private String storeProperties(Properties props, String header, boolean useWriter) throws IOException { DefaultPropertiesPersister persister = new DefaultPropertiesPersister(); - String propCopy = null; + String propCopy; if (useWriter) { StringWriter propWriter = new StringWriter(); persister.store(props, propWriter, header); diff --git a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java index 92c1595b57f7..429df4d0a449 100644 --- a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java +++ b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,29 @@ package org.springframework.util; import java.util.Properties; +import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** + * Tests for {@link PropertyPlaceholderHelper}. + * * @author Rob Harrop + * @author Stephane Nicoll */ class PropertyPlaceholderHelperTests { @@ -106,6 +119,57 @@ void unresolvedPlaceholderAsError() { PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, false); assertThatIllegalArgumentException().isThrownBy(() -> helper.replacePlaceholders(text, props)); + + } + + @Nested + class DefaultValueTests { + + private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", true); + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("defaultValues") + void defaultValueIsApplied(String text, String value) { + Properties properties = new Properties(); + properties.setProperty("one", "1"); + properties.setProperty("two", "2"); + assertThat(this.helper.replacePlaceholders(text, properties)).isEqualTo(value); + } + + @Test + @Disabled("gh-26268") + void defaultValueIsNotEvaluatedEarly() { + PlaceholderResolver resolver = mockPlaceholderResolver("one", "1"); + assertThat(this.helper.replacePlaceholders("This is ${one:or${two}}",resolver)).isEqualTo("This is 1"); + verify(resolver).resolvePlaceholder("one"); + verifyNoMoreInteractions(resolver); + } + + static Stream defaultValues() { + return Stream.of( + Arguments.of("${invalid:test}", "test"), + Arguments.of("${invalid:${one}}", "1"), + Arguments.of("${invalid:${one}${two}}", "12"), + Arguments.of("${invalid:${one}:${two}}", "1:2"), + Arguments.of("${invalid:${also_invalid:test}}", "test"), + Arguments.of("${invalid:${also_invalid:${one}}}", "1") + ); + } + + } + + PlaceholderResolver mockPlaceholderResolver(String... pairs) { + if (pairs.length % 2 == 1) { + throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); + } + PlaceholderResolver resolver = mock(PlaceholderResolver.class); + for (int i = 0; i < pairs.length; i += 2) { + String key = pairs[i]; + String value = pairs[i + 1]; + given(resolver.resolvePlaceholder(key)).willReturn(value); + } + return resolver; } + } diff --git a/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java index c702f095183f..c962f096d0a0 100644 --- a/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java @@ -73,7 +73,7 @@ void setField() { assertThat(testBean.getName()).isEqualTo("FooBar"); ReflectionUtils.setField(field, testBean, null); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); } @Test @@ -334,7 +334,7 @@ private static class ListSavingMethodCallback implements ReflectionUtils.MethodC private List methods = new ArrayList<>(); @Override - public void doWith(Method m) throws IllegalArgumentException, IllegalAccessException { + public void doWith(Method m) throws IllegalArgumentException { this.methodNames.add(m.getName()); this.methods.add(m); } @@ -378,7 +378,7 @@ private static class TestObjectSubclassWithFinalField extends TestObject { private static class A { - @SuppressWarnings("unused") + @SuppressWarnings({ "unused", "RedundantThrows" }) private void foo(Integer i) throws RemoteException { } } diff --git a/spring-core/src/test/java/org/springframework/util/ResizableByteArrayOutputStreamTests.java b/spring-core/src/test/java/org/springframework/util/ResizableByteArrayOutputStreamTests.java index 8646aba648cd..047edd7aa281 100644 --- a/spring-core/src/test/java/org/springframework/util/ResizableByteArrayOutputStreamTests.java +++ b/spring-core/src/test/java/org/springframework/util/ResizableByteArrayOutputStreamTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.util; +import java.nio.charset.StandardCharsets; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,7 +40,7 @@ class ResizableByteArrayOutputStreamTests { @BeforeEach void setUp() throws Exception { this.baos = new ResizableByteArrayOutputStream(INITIAL_CAPACITY); - this.helloBytes = "Hello World".getBytes("UTF-8"); + this.helloBytes = "Hello World".getBytes(StandardCharsets.UTF_8); } diff --git a/spring-core/src/test/java/org/springframework/util/ResourceUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ResourceUtilsTests.java index dc7b8339ac78..634969566d8c 100644 --- a/spring-core/src/test/java/org/springframework/util/ResourceUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ResourceUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.util; -import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; @@ -77,7 +76,7 @@ void extractArchiveURL() throws Exception { private static class DummyURLStreamHandler extends URLStreamHandler { @Override - protected URLConnection openConnection(URL url) throws IOException { + protected URLConnection openConnection(URL url) { throw new UnsupportedOperationException(); } } diff --git a/spring-core/src/test/java/org/springframework/util/SerializationUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SerializationUtilsTests.java index 12db0b1476af..5b655a6f2087 100644 --- a/spring-core/src/test/java/org/springframework/util/SerializationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/SerializationUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for {@link SerializationUtils}. + * Tests for {@link SerializationUtils}. * * @author Dave Syer * @author Sam Brannen @@ -47,7 +47,6 @@ void serializeCycleSunnyDay() { } @Test - @SuppressWarnings("deprecation") void serializeNonSerializableRecord() { record Person(String firstName, String lastName) {} Person jane = new Person("Jane", "Doe"); diff --git a/spring-core/src/test/java/org/springframework/util/StopWatchTests.java b/spring-core/src/test/java/org/springframework/util/StopWatchTests.java index 396a06263b48..2455b5e7fd73 100644 --- a/spring-core/src/test/java/org/springframework/util/StopWatchTests.java +++ b/spring-core/src/test/java/org/springframework/util/StopWatchTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for {@link StopWatch}. + * Tests for {@link StopWatch}. * * @author Rod Johnson * @author Juergen Hoeller @@ -47,7 +47,7 @@ class StopWatchTests { @Test void failureToStartBeforeGettingTimings() { - assertThatIllegalStateException().isThrownBy(stopWatch::getLastTaskTimeMillis); + assertThatIllegalStateException().isThrownBy(stopWatch::lastTaskInfo); } @Test diff --git a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java index 24330cf6778d..b17b5d56908a 100644 --- a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ * Tests for {@link StreamUtils}. * * @author Phillip Webb + * @author Juergen Hoeller */ class StreamUtilsTests { @@ -45,6 +46,7 @@ class StreamUtilsTests { private String string = ""; + @BeforeEach void setup() { new Random().nextBytes(bytes); @@ -53,6 +55,7 @@ void setup() { } } + @Test void copyToByteArray() throws Exception { InputStream inputStream = new ByteArrayInputStream(bytes); @@ -91,11 +94,30 @@ void copyStream() throws Exception { } @Test - void copyRange() throws Exception { + void copyRangeWithinBuffer() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + StreamUtils.copyRange(in, out, 0, 100); + assertThat(in.available()).isEqualTo(bytes.length - 101); + assertThat(out.toByteArray()).isEqualTo(Arrays.copyOfRange(bytes, 0, 101)); + } + + @Test + void copyRangeBeyondBuffer() throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); - StreamUtils.copyRange(new ByteArrayInputStream(bytes), out, 0, 100); - byte[] range = Arrays.copyOfRange(bytes, 0, 101); - assertThat(out.toByteArray()).isEqualTo(range); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + StreamUtils.copyRange(in, out, 0, 8200); + assertThat(in.available()).isEqualTo(1); + assertThat(out.toByteArray()).isEqualTo(Arrays.copyOfRange(bytes, 0, 8201)); + } + + @Test + void copyRangeBeyondAvailable() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + StreamUtils.copyRange(in, out, 0, 8300); + assertThat(in.available()).isEqualTo(0); + assertThat(out.toByteArray()).isEqualTo(Arrays.copyOfRange(bytes, 0, 8202)); } @Test @@ -127,4 +149,5 @@ void nonClosingOutputStream() throws Exception { ordered.verify(source).write(bytes, 1, 2); ordered.verify(source, never()).close(); } + } diff --git a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java index 614f0dfad33a..e9d055fbd48d 100644 --- a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java @@ -419,6 +419,7 @@ void cleanPath() { assertThat(StringUtils.cleanPath("file:///c:/some/../path/the%20file.txt")).isEqualTo("file:///c:/path/the%20file.txt"); assertThat(StringUtils.cleanPath("jar:file:///c:\\some\\..\\path\\.\\the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt"); assertThat(StringUtils.cleanPath("jar:file:///c:/some/../path/./the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt"); + assertThat(StringUtils.cleanPath("jar:file:///c:\\\\some\\\\..\\\\path\\\\.\\\\the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt"); } @Test diff --git a/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java index 30e70d2f7e06..6761a94d6681 100644 --- a/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java @@ -37,7 +37,7 @@ void replaceFromSystemProperty() { assertThat(resolved).isEqualTo("bar"); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } @@ -49,7 +49,7 @@ void replaceFromSystemPropertyWithDefault() { assertThat(resolved).isEqualTo("bar"); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } @@ -61,7 +61,7 @@ void replaceFromSystemPropertyWithExpressionDefault() { assertThat(resolved).isEqualTo("bar"); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } @@ -73,7 +73,7 @@ void replaceFromSystemPropertyWithExpressionContainingDefault() { assertThat(resolved).isEqualTo("bar"); } finally { - System.getProperties().remove("test.prop"); + System.clearProperty("test.prop"); } } @@ -122,8 +122,8 @@ void recursiveFromSystemProperty() { assertThat(resolved).isEqualTo("foo=baz"); } finally { - System.getProperties().remove("test.prop"); - System.getProperties().remove("bar"); + System.clearProperty("test.prop"); + System.clearProperty("bar"); } } diff --git a/spring-core/src/test/java/org/springframework/util/TypeUtilsTests.java b/spring-core/src/test/java/org/springframework/util/TypeUtilsTests.java index 221f26428e86..e5a5903dccc1 100644 --- a/spring-core/src/test/java/org/springframework/util/TypeUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/TypeUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link TypeUtils}. + * Tests for {@link TypeUtils}. * * @author Juergen Hoeller * @author Chris Beams diff --git a/spring-core/src/test/java/org/springframework/util/UnmodifiableMultiValueMapTests.java b/spring-core/src/test/java/org/springframework/util/UnmodifiableMultiValueMapTests.java index 473c49f5dad7..51d44a57c504 100644 --- a/spring-core/src/test/java/org/springframework/util/UnmodifiableMultiValueMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/UnmodifiableMultiValueMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for {@link UnmodifiableMultiValueMap}. + * Tests for {@link UnmodifiableMultiValueMap}. * * @author Arjen Poutsma * @since 6.0 @@ -41,7 +41,6 @@ class UnmodifiableMultiValueMapTests { @Test - @SuppressWarnings("unchecked") void delegation() { MultiValueMap mock = mock(); UnmodifiableMultiValueMap map = new UnmodifiableMultiValueMap<>(mock); @@ -50,7 +49,7 @@ void delegation() { assertThat(map).hasSize(1); given(mock.isEmpty()).willReturn(false); - assertThat(map.isEmpty()).isFalse(); + assertThat(map).isNotEmpty(); given(mock.containsKey("foo")).willReturn(true); assertThat(map.containsKey("foo")).isTrue(); @@ -95,7 +94,7 @@ void unsupported() { () -> map.computeIfPresent("foo", (s1, s2) -> List.of("bar"))); assertThatUnsupportedOperationException().isThrownBy(() -> map.compute("foo", (s1, s2) -> List.of("bar"))); assertThatUnsupportedOperationException().isThrownBy(() -> map.merge("foo", List.of("bar"), (s1, s2) -> s1)); - assertThatUnsupportedOperationException().isThrownBy(() -> map.clear()); + assertThatUnsupportedOperationException().isThrownBy(map::clear); } @Test @@ -137,7 +136,7 @@ void entrySetUnsupported() { assertThatUnsupportedOperationException().isThrownBy(() -> set.addAll(mock(List.class))); assertThatUnsupportedOperationException().isThrownBy(() -> set.retainAll(mock(List.class))); assertThatUnsupportedOperationException().isThrownBy(() -> set.removeAll(mock(List.class))); - assertThatUnsupportedOperationException().isThrownBy(() -> set.clear()); + assertThatUnsupportedOperationException().isThrownBy(set::clear); } @Test @@ -177,7 +176,7 @@ void valuesUnsupported() { assertThatUnsupportedOperationException().isThrownBy(() -> values.removeAll(List.of(List.of("foo")))); assertThatUnsupportedOperationException().isThrownBy(() -> values.retainAll(List.of(List.of("foo")))); assertThatUnsupportedOperationException().isThrownBy(() -> values.removeIf(s -> true)); - assertThatUnsupportedOperationException().isThrownBy(() -> values.clear()); + assertThatUnsupportedOperationException().isThrownBy(values::clear); } private static ThrowableTypeAssert assertThatUnsupportedOperationException() { diff --git a/spring-core/src/test/java/org/springframework/util/comparator/BooleanComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/BooleanComparatorTests.java index ae003d52c16f..bb3b766160dd 100644 --- a/spring-core/src/test/java/org/springframework/util/comparator/BooleanComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/util/comparator/BooleanComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,34 +29,35 @@ * @author Keith Donald * @author Chris Beams * @author Phillip Webb + * @author Eugene Rabii */ class BooleanComparatorTests { @Test void shouldCompareWithTrueLow() { Comparator c = new BooleanComparator(true); - assertThat(c.compare(true, false)).isEqualTo(-1); + assertThat(c.compare(true, false)).isLessThan(0); assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); } @Test void shouldCompareWithTrueHigh() { Comparator c = new BooleanComparator(false); - assertThat(c.compare(true, false)).isEqualTo(1); + assertThat(c.compare(true, false)).isGreaterThan(0); assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); } @Test void shouldCompareFromTrueLow() { Comparator c = BooleanComparator.TRUE_LOW; - assertThat(c.compare(true, false)).isEqualTo(-1); + assertThat(c.compare(true, false)).isLessThan(0); assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); } @Test void shouldCompareFromTrueHigh() { Comparator c = BooleanComparator.TRUE_HIGH; - assertThat(c.compare(true, false)).isEqualTo(1); + assertThat(c.compare(true, false)).isGreaterThan(0); assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); } diff --git a/spring-core/src/test/java/org/springframework/util/comparator/ComparableComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/ComparableComparatorTests.java index 5171625fa2a5..b6becdc0fe34 100644 --- a/spring-core/src/test/java/org/springframework/util/comparator/ComparableComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/util/comparator/ComparableComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,24 +30,24 @@ * @author Chris Beams * @author Phillip Webb */ +@Deprecated class ComparableComparatorTests { @Test void comparableComparator() { + @SuppressWarnings("deprecation") Comparator c = new ComparableComparator<>(); - String s1 = "abc"; - String s2 = "cde"; - assertThat(c.compare(s1, s2)).isLessThan(0); + assertThat(c.compare("abc", "cde")).isLessThan(0); } - @SuppressWarnings({ "unchecked", "rawtypes" }) @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) void shouldNeedComparable() { + @SuppressWarnings("deprecation") Comparator c = new ComparableComparator(); Object o1 = new Object(); Object o2 = new Object(); - assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> - c.compare(o1, o2)); + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> c.compare(o1, o2)); } } diff --git a/spring-core/src/test/java/org/springframework/util/comparator/ComparatorsTests.java b/spring-core/src/test/java/org/springframework/util/comparator/ComparatorsTests.java new file mode 100644 index 000000000000..ca53f6851310 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/comparator/ComparatorsTests.java @@ -0,0 +1,64 @@ +/* + * 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.util.comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Comparators}. + * + * @since 6.1.2 + * @author Mathieu Amblard + * @author Sam Brannen + */ +class ComparatorsTests { + + @Test + void nullsLow() { + assertThat(Comparators.nullsLow().compare("boo", "boo")).isZero(); + assertThat(Comparators.nullsLow().compare(null, null)).isZero(); + assertThat(Comparators.nullsLow().compare(null, "boo")).isNegative(); + assertThat(Comparators.nullsLow().compare("boo", null)).isPositive(); + } + + @Test + void nullsLowWithExplicitComparator() { + assertThat(Comparators.nullsLow(String::compareTo).compare("boo", "boo")).isZero(); + assertThat(Comparators.nullsLow(String::compareTo).compare(null, null)).isZero(); + assertThat(Comparators.nullsLow(String::compareTo).compare(null, "boo")).isNegative(); + assertThat(Comparators.nullsLow(String::compareTo).compare("boo", null)).isPositive(); + } + + @Test + void nullsHigh() { + assertThat(Comparators.nullsHigh().compare("boo", "boo")).isZero(); + assertThat(Comparators.nullsHigh().compare(null, null)).isZero(); + assertThat(Comparators.nullsHigh().compare(null, "boo")).isPositive(); + assertThat(Comparators.nullsHigh().compare("boo", null)).isNegative(); + } + + @Test + void nullsHighWithExplicitComparator() { + assertThat(Comparators.nullsHigh(String::compareTo).compare("boo", "boo")).isZero(); + assertThat(Comparators.nullsHigh(String::compareTo).compare(null, null)).isZero(); + assertThat(Comparators.nullsHigh(String::compareTo).compare(null, "boo")).isPositive(); + assertThat(Comparators.nullsHigh(String::compareTo).compare("boo", null)).isNegative(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/comparator/InstanceComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/InstanceComparatorTests.java index 4d417c41310a..fb1a015a26f5 100644 --- a/spring-core/src/test/java/org/springframework/util/comparator/InstanceComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/util/comparator/InstanceComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ class InstanceComparatorTests { private C4 c4 = new C4(); @Test - void shouldCompareClasses() throws Exception { + void shouldCompareClasses() { Comparator comparator = new InstanceComparator<>(C1.class, C2.class); assertThat(comparator.compare(c1, c1)).isEqualTo(0); assertThat(comparator.compare(c1, c2)).isEqualTo(-1); @@ -50,7 +50,7 @@ void shouldCompareClasses() throws Exception { } @Test - void shouldCompareInterfaces() throws Exception { + void shouldCompareInterfaces() { Comparator comparator = new InstanceComparator<>(I1.class, I2.class); assertThat(comparator.compare(c1, c1)).isEqualTo(0); assertThat(comparator.compare(c1, c2)).isEqualTo(0); @@ -61,7 +61,7 @@ void shouldCompareInterfaces() throws Exception { } @Test - void shouldCompareMix() throws Exception { + void shouldCompareMix() { Comparator comparator = new InstanceComparator<>(I1.class, C3.class); assertThat(comparator.compare(c1, c1)).isEqualTo(0); assertThat(comparator.compare(c3, c4)).isEqualTo(-1); diff --git a/spring-core/src/test/java/org/springframework/util/comparator/NullSafeComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/NullSafeComparatorTests.java index 83a41b1154ee..bb16188e5d16 100644 --- a/spring-core/src/test/java/org/springframework/util/comparator/NullSafeComparatorTests.java +++ b/spring-core/src/test/java/org/springframework/util/comparator/NullSafeComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,21 +29,31 @@ * @author Chris Beams * @author Phillip Webb */ +@Deprecated class NullSafeComparatorTests { - @SuppressWarnings("unchecked") @Test + @SuppressWarnings("unchecked") void shouldCompareWithNullsLow() { + @SuppressWarnings("deprecation") Comparator c = NullSafeComparator.NULLS_LOW; - assertThat(c.compare(null, "boo")).isLessThan(0); + + assertThat(c.compare("boo", "boo")).isZero(); + assertThat(c.compare(null, null)).isZero(); + assertThat(c.compare(null, "boo")).isNegative(); + assertThat(c.compare("boo", null)).isPositive(); } - @SuppressWarnings("unchecked") @Test + @SuppressWarnings("unchecked") void shouldCompareWithNullsHigh() { + @SuppressWarnings("deprecation") Comparator c = NullSafeComparator.NULLS_HIGH; - assertThat(c.compare(null, "boo")).isGreaterThan(0); - assertThat(c.compare(null, null)).isEqualTo(0); + + assertThat(c.compare("boo", "boo")).isZero(); + assertThat(c.compare(null, null)).isZero(); + assertThat(c.compare(null, "boo")).isPositive(); + assertThat(c.compare("boo", null)).isNegative(); } } diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/FutureAdapterTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/FutureAdapterTests.java index b892d64ece39..e0e68859362a 100644 --- a/spring-core/src/test/java/org/springframework/util/concurrent/FutureAdapterTests.java +++ b/spring-core/src/test/java/org/springframework/util/concurrent/FutureAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.util.concurrent; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -32,12 +31,11 @@ @SuppressWarnings("deprecation") class FutureAdapterTests { - @SuppressWarnings("unchecked") private Future adaptee = mock(); private FutureAdapter adapter = new FutureAdapter<>(adaptee) { @Override - protected String adapt(Integer adapteeResult) throws ExecutionException { + protected String adapt(Integer adapteeResult) { return adapteeResult.toString(); } }; diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java index 815fdfbf7310..01e08bc6ac46 100644 --- a/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java +++ b/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ * @author Arjen Poutsma * @author Sebastien Deleuze */ -@SuppressWarnings({ "unchecked", "deprecation" }) +@SuppressWarnings({ "deprecation" }) class ListenableFutureTaskTests { @Test @@ -42,11 +42,12 @@ void success() throws Exception { Callable callable = () -> s; ListenableFutureTask task = new ListenableFutureTask<>(callable); - task.addCallback(new ListenableFutureCallback() { + task.addCallback(new ListenableFutureCallback<>() { @Override public void onSuccess(String result) { assertThat(result).isEqualTo(s); } + @Override public void onFailure(Throwable ex) { throw new AssertionError(ex.getMessage(), ex); @@ -60,18 +61,19 @@ public void onFailure(Throwable ex) { } @Test - void failure() throws Exception { + void failure() { final String s = "Hello World"; Callable callable = () -> { throw new IOException(s); }; ListenableFutureTask task = new ListenableFutureTask<>(callable); - task.addCallback(new ListenableFutureCallback() { + task.addCallback(new ListenableFutureCallback<>() { @Override public void onSuccess(String result) { fail("onSuccess not expected"); } + @Override public void onFailure(Throwable ex) { assertThat(ex.getMessage()).isEqualTo(s); @@ -108,7 +110,7 @@ void successWithLambdas() throws Exception { } @Test - void failureWithLambdas() throws Exception { + void failureWithLambdas() { final String s = "Hello World"; IOException ex = new IOException(s); Callable callable = () -> { diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java index 7b527350d7dd..278451b46eff 100644 --- a/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java +++ b/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link MonoToListenableFutureAdapter}. + * Tests for {@link MonoToListenableFutureAdapter}. * * @author Rossen Stoyanchev */ diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java index 959d383d34a6..5fd9af58305a 100644 --- a/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java +++ b/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java @@ -75,7 +75,7 @@ void setValueUpdatesDoneStatus() { } @Test - void throwsSetExceptionWrappedInExecutionException() throws Exception { + void throwsSetExceptionWrappedInExecutionException() { Throwable exception = new RuntimeException(); assertThat(settableListenableFuture.setException(exception)).isTrue(); @@ -88,7 +88,7 @@ void throwsSetExceptionWrappedInExecutionException() throws Exception { } @Test - void throwsSetExceptionWrappedInExecutionExceptionFromCompletable() throws Exception { + void throwsSetExceptionWrappedInExecutionExceptionFromCompletable() { Throwable exception = new RuntimeException(); assertThat(settableListenableFuture.setException(exception)).isTrue(); Future completable = settableListenableFuture.completable(); @@ -102,7 +102,7 @@ void throwsSetExceptionWrappedInExecutionExceptionFromCompletable() throws Excep } @Test - void throwsSetErrorWrappedInExecutionException() throws Exception { + void throwsSetErrorWrappedInExecutionException() { Throwable exception = new OutOfMemoryError(); assertThat(settableListenableFuture.setException(exception)).isTrue(); @@ -115,7 +115,7 @@ void throwsSetErrorWrappedInExecutionException() throws Exception { } @Test - void throwsSetErrorWrappedInExecutionExceptionFromCompletable() throws Exception { + void throwsSetErrorWrappedInExecutionExceptionFromCompletable() { Throwable exception = new OutOfMemoryError(); assertThat(settableListenableFuture.setException(exception)).isTrue(); Future completable = settableListenableFuture.completable(); @@ -133,11 +133,12 @@ void setValueTriggersCallback() { String string = "hello"; final String[] callbackHolder = new String[1]; - settableListenableFuture.addCallback(new ListenableFutureCallback() { + settableListenableFuture.addCallback(new ListenableFutureCallback<>() { @Override public void onSuccess(String result) { callbackHolder[0] = result; } + @Override public void onFailure(Throwable ex) { throw new AssertionError("Expected onSuccess() to be called", ex); @@ -155,11 +156,12 @@ void setValueTriggersCallbackOnlyOnce() { String string = "hello"; final String[] callbackHolder = new String[1]; - settableListenableFuture.addCallback(new ListenableFutureCallback() { + settableListenableFuture.addCallback(new ListenableFutureCallback<>() { @Override public void onSuccess(String result) { callbackHolder[0] = result; } + @Override public void onFailure(Throwable ex) { throw new AssertionError("Expected onSuccess() to be called", ex); @@ -178,11 +180,12 @@ void setExceptionTriggersCallback() { Throwable exception = new RuntimeException(); final Throwable[] callbackHolder = new Throwable[1]; - settableListenableFuture.addCallback(new ListenableFutureCallback() { + settableListenableFuture.addCallback(new ListenableFutureCallback<>() { @Override public void onSuccess(String result) { fail("Expected onFailure() to be called"); } + @Override public void onFailure(Throwable ex) { callbackHolder[0] = ex; @@ -200,11 +203,12 @@ void setExceptionTriggersCallbackOnlyOnce() { Throwable exception = new RuntimeException(); final Throwable[] callbackHolder = new Throwable[1]; - settableListenableFuture.addCallback(new ListenableFutureCallback() { + settableListenableFuture.addCallback(new ListenableFutureCallback<>() { @Override public void onSuccess(String result) { fail("Expected onFailure() to be called"); } + @Override public void onFailure(Throwable ex) { callbackHolder[0] = ex; @@ -221,7 +225,7 @@ public void onFailure(Throwable ex) { @Test void nullIsAcceptedAsValueToSet() throws ExecutionException, InterruptedException { settableListenableFuture.set(null); - assertThat((Object) settableListenableFuture.get()).isNull(); + assertThat(settableListenableFuture.get()).isNull(); assertThat(settableListenableFuture.isCancelled()).isFalse(); assertThat(settableListenableFuture.isDone()).isTrue(); } @@ -247,7 +251,7 @@ void getWaitsForCompletion() throws ExecutionException, InterruptedException { } @Test - void getWithTimeoutThrowsTimeoutException() throws ExecutionException, InterruptedException { + void getWithTimeoutThrowsTimeoutException() { assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> settableListenableFuture.get(1L, TimeUnit.MILLISECONDS)); } @@ -330,7 +334,7 @@ void setExceptionPreventsCancel() { } @Test - void cancelStateThrowsExceptionWhenCallingGet() throws ExecutionException, InterruptedException { + void cancelStateThrowsExceptionWhenCallingGet() { settableListenableFuture.cancel(true); assertThatExceptionOfType(CancellationException.class).isThrownBy(settableListenableFuture::get); @@ -340,7 +344,7 @@ void cancelStateThrowsExceptionWhenCallingGet() throws ExecutionException, Inter } @Test - void cancelStateThrowsExceptionWhenCallingGetWithTimeout() throws ExecutionException, TimeoutException, InterruptedException { + void cancelStateThrowsExceptionWhenCallingGetWithTimeout() { new Thread(() -> { try { Thread.sleep(20L); diff --git a/spring-core/src/test/java/org/springframework/util/function/ThrowingBiFunctionTests.java b/spring-core/src/test/java/org/springframework/util/function/ThrowingBiFunctionTests.java index 93d4a55a2bfd..07c2e6c85a99 100644 --- a/spring-core/src/test/java/org/springframework/util/function/ThrowingBiFunctionTests.java +++ b/spring-core/src/test/java/org/springframework/util/function/ThrowingBiFunctionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ private Object throwIOException(Object o, Object u) throws IOException { throw new IOException(); } - private Object throwIllegalArgumentException(Object o, Object u) throws IOException { + private Object throwIllegalArgumentException(Object o, Object u) { throw new IllegalArgumentException(); } diff --git a/spring-core/src/test/java/org/springframework/util/function/ThrowingConsumerTests.java b/spring-core/src/test/java/org/springframework/util/function/ThrowingConsumerTests.java index d1892d1d5a8a..2d60b43ea903 100644 --- a/spring-core/src/test/java/org/springframework/util/function/ThrowingConsumerTests.java +++ b/spring-core/src/test/java/org/springframework/util/function/ThrowingConsumerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ private void throwIOException(Object o) throws IOException { throw new IOException(); } - private void throwIllegalArgumentException(Object o) throws IOException { + private void throwIllegalArgumentException(Object o) { throw new IllegalArgumentException(); } diff --git a/spring-core/src/test/java/org/springframework/util/function/ThrowingFunctionTests.java b/spring-core/src/test/java/org/springframework/util/function/ThrowingFunctionTests.java index c985c23954a3..389ea284d78a 100644 --- a/spring-core/src/test/java/org/springframework/util/function/ThrowingFunctionTests.java +++ b/spring-core/src/test/java/org/springframework/util/function/ThrowingFunctionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ private Object throwIOException(Object o) throws IOException { throw new IOException(); } - private Object throwIllegalArgumentException(Object o) throws IOException { + private Object throwIllegalArgumentException(Object o) { throw new IllegalArgumentException(); } diff --git a/spring-core/src/test/java/org/springframework/util/function/ThrowingSupplierTests.java b/spring-core/src/test/java/org/springframework/util/function/ThrowingSupplierTests.java index 7e9ded1255b3..ab6a0e5957f4 100644 --- a/spring-core/src/test/java/org/springframework/util/function/ThrowingSupplierTests.java +++ b/spring-core/src/test/java/org/springframework/util/function/ThrowingSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ void throwingModifiesThrownException() { ThrowingSupplier modified = supplier.throwing( IllegalStateException::new); assertThatIllegalStateException().isThrownBy( - () -> modified.get()).withCauseInstanceOf(IOException.class); + modified::get).withCauseInstanceOf(IOException.class); } @Test @@ -74,14 +74,14 @@ void ofModifiesThrowException() { ThrowingSupplier supplier = ThrowingSupplier.of( this::throwIOException, IllegalStateException::new); assertThatIllegalStateException().isThrownBy( - () -> supplier.get()).withCauseInstanceOf(IOException.class); + supplier::get).withCauseInstanceOf(IOException.class); } private Object throwIOException() throws IOException { throw new IOException(); } - private Object throwIllegalArgumentException() throws IOException { + private Object throwIllegalArgumentException() { throw new IllegalArgumentException(); } diff --git a/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java index 655a6343976a..7f51d8af088a 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; @@ -133,7 +134,7 @@ void whitespace() throws Exception { Transformer transformer = TransformerFactory.newInstance().newTransformer(); AbstractStaxXMLReader staxXmlReader = createStaxXmlReader( - new ByteArrayInputStream(xml.getBytes("UTF-8"))); + new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); SAXSource source = new SAXSource(staxXmlReader, new InputSource()); DOMResult result = new DOMResult(); @@ -243,7 +244,7 @@ public Object[] adaptArguments(Object[] arguments) { private static class CopyCharsAnswer implements Answer { @Override - public Object answer(InvocationOnMock invocation) throws Throwable { + public Object answer(InvocationOnMock invocation) { char[] chars = (char[]) invocation.getArguments()[0]; char[] copy = new char[chars.length]; System.arraycopy(chars, 0, copy, 0, chars.length); diff --git a/spring-core/src/test/java/org/springframework/util/xml/DomContentHandlerTests.java b/spring-core/src/test/java/org/springframework/util/xml/DomContentHandlerTests.java index 88b6d8c6b327..2621be76c498 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/DomContentHandlerTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/DomContentHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link DomContentHandler}. + * Tests for {@link DomContentHandler}. */ class DomContentHandlerTests { diff --git a/spring-core/src/test/java/org/springframework/util/xml/SimpleNamespaceContextTests.java b/spring-core/src/test/java/org/springframework/util/xml/SimpleNamespaceContextTests.java index 5eb06f5eca04..8b1695b109e9 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/SimpleNamespaceContextTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/SimpleNamespaceContextTests.java @@ -45,7 +45,7 @@ class SimpleNamespaceContextTests { @Test - void getNamespaceURI_withNull() throws Exception { + void getNamespaceURI_withNull() { assertThatIllegalArgumentException().isThrownBy(() -> context.getNamespaceURI(null)); } @@ -77,7 +77,7 @@ void getNamespaceURI() { } @Test - void getPrefix_withNull() throws Exception { + void getPrefix_withNull() { assertThatIllegalArgumentException().isThrownBy(() -> context.getPrefix(null)); } @@ -100,13 +100,13 @@ void getPrefix() { } @Test - void getPrefixes_withNull() throws Exception { + void getPrefixes_withNull() { assertThatIllegalArgumentException().isThrownBy(() -> context.getPrefixes(null)); } @Test - void getPrefixes_IteratorIsNotModifiable() throws Exception { + void getPrefixes_IteratorIsNotModifiable() { context.bindNamespaceUri(prefix, namespaceUri); Iterator iterator = context.getPrefixes(namespaceUri); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxSourceTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxSourceTests.java index dc82a03c3922..f161bd9ff3e0 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/StaxSourceTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ void streamReaderSourceToStreamResult() throws Exception { XMLStreamReader streamReader = inputFactory.createXMLStreamReader(new StringReader(XML)); StaxSource source = new StaxSource(streamReader); assertThat(source.getXMLStreamReader()).as("Invalid streamReader returned").isEqualTo(streamReader); - assertThat((Object) source.getXMLEventReader()).as("EventReader returned").isNull(); + assertThat(source.getXMLEventReader()).as("EventReader returned").isNull(); StringWriter writer = new StringWriter(); transformer.transform(source, new StreamResult(writer)); assertThat(XmlContent.from(writer)).as("Invalid result").isSimilarTo(XML); @@ -77,7 +77,7 @@ void streamReaderSourceToDOMResult() throws Exception { XMLStreamReader streamReader = inputFactory.createXMLStreamReader(new StringReader(XML)); StaxSource source = new StaxSource(streamReader); assertThat(source.getXMLStreamReader()).as("Invalid streamReader returned").isEqualTo(streamReader); - assertThat((Object) source.getXMLEventReader()).as("EventReader returned").isNull(); + assertThat(source.getXMLEventReader()).as("EventReader returned").isNull(); Document expected = documentBuilder.parse(new InputSource(new StringReader(XML))); Document result = documentBuilder.newDocument(); @@ -89,7 +89,7 @@ void streamReaderSourceToDOMResult() throws Exception { void eventReaderSourceToStreamResult() throws Exception { XMLEventReader eventReader = inputFactory.createXMLEventReader(new StringReader(XML)); StaxSource source = new StaxSource(eventReader); - assertThat((Object) source.getXMLEventReader()).as("Invalid eventReader returned").isEqualTo(eventReader); + assertThat(source.getXMLEventReader()).as("Invalid eventReader returned").isEqualTo(eventReader); assertThat(source.getXMLStreamReader()).as("StreamReader returned").isNull(); StringWriter writer = new StringWriter(); transformer.transform(source, new StreamResult(writer)); @@ -100,7 +100,7 @@ void eventReaderSourceToStreamResult() throws Exception { void eventReaderSourceToDOMResult() throws Exception { XMLEventReader eventReader = inputFactory.createXMLEventReader(new StringReader(XML)); StaxSource source = new StaxSource(eventReader); - assertThat((Object) source.getXMLEventReader()).as("Invalid eventReader returned").isEqualTo(eventReader); + assertThat(source.getXMLEventReader()).as("Invalid eventReader returned").isEqualTo(eventReader); assertThat(source.getXMLStreamReader()).as("StreamReader returned").isNull(); Document expected = documentBuilder.parse(new InputSource(new StringReader(XML))); @@ -108,4 +108,5 @@ void eventReaderSourceToDOMResult() throws Exception { transformer.transform(source, new DOMResult(result)); assertThat(XmlContent.of(result)).as("Invalid result").isSimilarTo(expected); } + } diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxUtilsTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxUtilsTests.java index 959bc0b8fb0c..327248dd8462 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/StaxUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ class StaxUtilsTests { @Test - void isStaxSourceInvalid() throws Exception { + void isStaxSourceInvalid() { assertThat(StaxUtils.isStaxSource(new DOMSource())).as("A StAX Source").isFalse(); assertThat(StaxUtils.isStaxSource(new SAXSource())).as("A StAX Source").isFalse(); assertThat(StaxUtils.isStaxSource(new StreamSource())).as("A StAX Source").isFalse(); @@ -71,7 +71,7 @@ void isStaxSourceJaxp14() throws Exception { } @Test - void isStaxResultInvalid() throws Exception { + void isStaxResultInvalid() { assertThat(StaxUtils.isStaxResult(new DOMResult())).as("A StAX Result").isFalse(); assertThat(StaxUtils.isStaxResult(new SAXResult())).as("A StAX Result").isFalse(); assertThat(StaxUtils.isStaxResult(new StreamResult())).as("A StAX Result").isFalse(); diff --git a/spring-core/src/test/java/org/springframework/util/xml/TransformerUtilsTests.java b/spring-core/src/test/java/org/springframework/util/xml/TransformerUtilsTests.java index b4de128c01cd..dd2493c8c3c5 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/TransformerUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/TransformerUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link TransformerUtils}. + * Tests for {@link TransformerUtils}. * * @author Rick Evans * @author Arjen Poutsma @@ -40,7 +40,7 @@ class TransformerUtilsTests { @Test - void enableIndentingSunnyDay() throws Exception { + void enableIndentingSunnyDay() { Transformer transformer = new StubTransformer(); TransformerUtils.enableIndenting(transformer); String indent = transformer.getOutputProperty(OutputKeys.INDENT); @@ -52,7 +52,7 @@ void enableIndentingSunnyDay() throws Exception { } @Test - void enableIndentingSunnyDayWithCustomKosherIndentAmount() throws Exception { + void enableIndentingSunnyDayWithCustomKosherIndentAmount() { final String indentAmountProperty = "10"; Transformer transformer = new StubTransformer(); TransformerUtils.enableIndenting(transformer, Integer.parseInt(indentAmountProperty)); @@ -65,7 +65,7 @@ void enableIndentingSunnyDayWithCustomKosherIndentAmount() throws Exception { } @Test - void disableIndentingSunnyDay() throws Exception { + void disableIndentingSunnyDay() { Transformer transformer = new StubTransformer(); TransformerUtils.disableIndenting(transformer); String indent = transformer.getOutputProperty(OutputKeys.INDENT); @@ -74,25 +74,25 @@ void disableIndentingSunnyDay() throws Exception { } @Test - void enableIndentingWithNullTransformer() throws Exception { + void enableIndentingWithNullTransformer() { assertThatIllegalArgumentException().isThrownBy(() -> TransformerUtils.enableIndenting(null)); } @Test - void disableIndentingWithNullTransformer() throws Exception { + void disableIndentingWithNullTransformer() { assertThatIllegalArgumentException().isThrownBy(() -> TransformerUtils.disableIndenting(null)); } @Test - void enableIndentingWithNegativeIndentAmount() throws Exception { + void enableIndentingWithNegativeIndentAmount() { assertThatIllegalArgumentException().isThrownBy(() -> TransformerUtils.enableIndenting(new StubTransformer(), -21938)); } @Test - void enableIndentingWithZeroIndentAmount() throws Exception { + void enableIndentingWithZeroIndentAmount() { TransformerUtils.enableIndenting(new StubTransformer(), 0); } diff --git a/spring-core/src/test/java/org/springframework/util/xml/XmlValidationModeDetectorTests.java b/spring-core/src/test/java/org/springframework/util/xml/XmlValidationModeDetectorTests.java index 35a5e13222ca..2cbb32bab5fe 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/XmlValidationModeDetectorTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/XmlValidationModeDetectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import static org.springframework.util.xml.XmlValidationModeDetector.VALIDATION_XSD; /** - * Unit tests for {@link XmlValidationModeDetector}. + * Tests for {@link XmlValidationModeDetector}. * * @author Sam Brannen * @since 5.1.10 diff --git a/spring-core/src/test/java21/org/springframework/core/task/VirtualThreadTaskExecutorTests.java b/spring-core/src/test/java21/org/springframework/core/task/VirtualThreadTaskExecutorTests.java new file mode 100644 index 000000000000..af110641d1ae --- /dev/null +++ b/spring-core/src/test/java21/org/springframework/core/task/VirtualThreadTaskExecutorTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.task; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 6.1 + */ +class VirtualThreadTaskExecutorTests { + + @Test + void virtualThreadsWithoutName() { + final Object monitor = new Object(); + VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor(); + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).isEmpty(); + assertThat(task.isVirtual()).isTrue(); + assertThat(task.runCount()).isOne(); + } + + @Test + void virtualThreadsWithNamePrefix() { + final Object monitor = new Object(); + VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("test-"); + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).isEqualTo("test-0"); + assertThat(task.isVirtual()).isTrue(); + assertThat(task.runCount()).isOne(); + } + + @Test + void simpleWithVirtualThreadFactory() { + final Object monitor = new Object(); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(Thread.ofVirtual().name("test").factory()); + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).isEqualTo("test"); + assertThat(task.isVirtual()).isTrue(); + assertThat(task.runCount()).isOne(); + } + + @Test + void simpleWithVirtualThreadFlag() { + final String customPrefix = "chankPop#"; + final Object monitor = new Object(); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix); + executor.setVirtualThreads(true); + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).startsWith(customPrefix); + assertThat(task.isVirtual()).isTrue(); + assertThat(task.runCount()).isOne(); + } + + private void executeAndWait(TaskExecutor executor, Runnable task, Object monitor) { + synchronized (monitor) { + executor.execute(task); + try { + monitor.wait(); + } + catch (InterruptedException ignored) { + } + } + } + + + private static final class NoOpRunnable implements Runnable { + + @Override + public void run() { + // no-op + } + } + + + private abstract static class AbstractNotifyingRunnable implements Runnable { + + private final Object monitor; + + protected AbstractNotifyingRunnable(Object monitor) { + this.monitor = monitor; + } + + @Override + public final void run() { + synchronized (this.monitor) { + try { + doRun(); + } + finally { + this.monitor.notifyAll(); + } + } + } + + protected abstract void doRun(); + } + + + private static final class ThreadNameHarvester extends AbstractNotifyingRunnable { + + private final AtomicInteger runCount = new AtomicInteger(); + + private String threadName; + + private boolean virtual; + + protected ThreadNameHarvester(Object monitor) { + super(monitor); + } + + public String getThreadName() { + return this.threadName; + } + + public boolean isVirtual() { + return this.virtual; + } + + public int runCount() { + return this.runCount.get(); + } + + @Override + protected void doRun() { + Thread thread = Thread.currentThread(); + this.threadName = thread.getName(); + this.virtual = thread.isVirtual(); + runCount.incrementAndGet(); + } + } + +} diff --git a/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt index b8e938186737..861f34325559 100644 --- a/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,8 +68,10 @@ class BindingReflectionHintsRegistrarKotlinTests { assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "component1")).accepts(hints) assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "copy")).accepts(hints) assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "getName")).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "isNonNullable")).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "isNullable")).accepts(hints) val copyDefault: Method = SampleDataClass::class.java.getMethod("copy\$default", SampleDataClass::class.java, - String::class.java , Int::class.java, Object::class.java) + String::class.java, Boolean::class.javaPrimitiveType, Boolean::class.javaObjectType, Int::class.java, Object::class.java) assertThat(RuntimeHintsPredicates.reflection().onMethod(copyDefault)).accepts(hints) } @@ -84,6 +86,6 @@ class BindingReflectionHintsRegistrarKotlinTests { @kotlinx.serialization.Serializable class SampleSerializableClass(val name: String) -data class SampleDataClass(val name: String) +data class SampleDataClass(val name: String, val isNonNullable: Boolean, val isNullable: Boolean?) class SampleClass(val name: String) diff --git a/spring-core/src/test/kotlin/org/springframework/aot/hint/ResourceHintsExtensionsTests.kt b/spring-core/src/test/kotlin/org/springframework/aot/hint/ResourceHintsExtensionsTests.kt index 8dcf832f899c..f02da9fd96b1 100644 --- a/spring-core/src/test/kotlin/org/springframework/aot/hint/ResourceHintsExtensionsTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/aot/hint/ResourceHintsExtensionsTests.kt @@ -20,7 +20,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Test -import java.util.function.Consumer /** * Tests for [ResourceHints] Kotlin extensions. diff --git a/spring-core/src/test/kotlin/org/springframework/core/AbstractReflectionParameterNameDiscovererKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/AbstractReflectionParameterNameDiscovererKotlinTests.kt new file mode 100644 index 000000000000..d84f6c321968 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/AbstractReflectionParameterNameDiscovererKotlinTests.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +import org.springframework.util.ReflectionUtils + +/** + * Abstract tests for Kotlin [ParameterNameDiscoverer] aware implementations. + * + * @author Sebastien Deleuze + */ +@Suppress("UNUSED_PARAMETER") +abstract class AbstractReflectionParameterNameDiscovererKotlinTests(protected val parameterNameDiscoverer: ParameterNameDiscoverer) { + + @Test + fun getParameterNamesOnInterface() { + val method = ReflectionUtils.findMethod(MessageService::class.java, "sendMessage", String::class.java)!! + val actualParams = parameterNameDiscoverer.getParameterNames(method) + assertThat(actualParams).contains("message") + } + + @Test + fun getParameterNamesOnClass() { + val constructor = ReflectionUtils.accessibleConstructor(MessageServiceImpl::class.java,String::class.java) + val actualConstructorParams = parameterNameDiscoverer.getParameterNames(constructor) + assertThat(actualConstructorParams).contains("message") + val method = ReflectionUtils.findMethod(MessageServiceImpl::class.java, "sendMessage", String::class.java)!! + val actualMethodParams = parameterNameDiscoverer.getParameterNames(method) + assertThat(actualMethodParams).contains("message") + } + + @Test + fun getParameterNamesOnExtensionMethod() { + val method = ReflectionUtils.findMethod(UtilityClass::class.java, "identity", String::class.java)!! + val actualParams = parameterNameDiscoverer.getParameterNames(method)!! + assertThat(actualParams).contains("\$receiver") + } + + interface MessageService { + fun sendMessage(message: String) + } + + class MessageServiceImpl(message: String) { + fun sendMessage(message: String) = message + } + + class UtilityClass { + fun String.identity() = this + } + +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt index ad7e4ac5d857..6aee679df09f 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import reactor.core.publisher.Flux @@ -27,6 +28,8 @@ import reactor.core.publisher.Mono import reactor.test.StepVerifier import kotlin.coroutines.Continuation import kotlin.coroutines.coroutineContext +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible /** * Kotlin tests for [CoroutinesUtils]. @@ -81,12 +84,44 @@ class CoroutinesUtilsTests { .verify() } + @Test + fun invokeSuspendingFunctionWithNullableParameter() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithNullable", String::class.java, Continuation::class.java) + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null, null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingleOrNull()).isNull() + } + } + @Test fun invokeNonSuspendingFunction() { val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("nonSuspendingFunction", String::class.java) Assertions.assertThatIllegalArgumentException().isThrownBy { CoroutinesUtils.invokeSuspendingFunction(method, this, "foo") } } + @Test + fun invokeSuspendingFunctionWithMono() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithMono", Continuation::class.java) + val publisher = CoroutinesUtils.invokeSuspendingFunction(method, this) + Assertions.assertThat(publisher).isInstanceOf(Mono::class.java) + StepVerifier.create(publisher) + .expectNext("foo") + .expectComplete() + .verify() + } + + @Test + fun invokeSuspendingFunctionWithFlux() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithFlux", Continuation::class.java) + val publisher = CoroutinesUtils.invokeSuspendingFunction(method, this) + Assertions.assertThat(publisher).isInstanceOf(Flux::class.java) + StepVerifier.create(publisher) + .expectNext("foo") + .expectNext("bar") + .expectComplete() + .verify() + } + @Test fun invokeSuspendingFunctionWithFlow() { val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithFlow", Continuation::class.java) @@ -126,11 +161,112 @@ class CoroutinesUtilsTests { Assertions.assertThatIllegalArgumentException().isThrownBy { CoroutinesUtils.invokeSuspendingFunction(context, method, this, "foo") } } + @Test + fun invokeSuspendingFunctionReturningUnit() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingUnit", Continuation::class.java) + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingleOrNull()).isNull() + } + } + + @Test + fun invokeSuspendingFunctionReturningNull() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingNullable", Continuation::class.java) + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingleOrNull()).isNull() + } + } + + @Test + fun invokeSuspendingFunctionWithValueClassParameter() { + val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClass") } + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo") + } + } + + @Test + fun invokeSuspendingFunctionWithValueClassWithInitParameter() { + val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassWithInit") } + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "", null) as Mono + Assertions.assertThatIllegalArgumentException().isThrownBy { + runBlocking { + mono.awaitSingle() + } + } + } + + @Test + fun invokeSuspendingFunctionWithNullableValueClassParameter() { + val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") } + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null, null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingleOrNull()).isNull() + } + } + + @Test + fun invokeSuspendingFunctionWithValueClassWithPrivateConstructorParameter() { + val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassWithPrivateConstructor") } + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo") + } + } + + @Test + fun invokeSuspendingFunctionWithExtension() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithExtension", + CustomException::class.java, Continuation::class.java) + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, CustomException("foo")) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo") + } + } + + @Test + fun invokeSuspendingFunctionWithExtensionAndParameter() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithExtensionAndParameter", + CustomException::class.java, Int::class.java, Continuation::class.java) + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, CustomException("foo"), 20) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo-20") + } + } + + @Test + fun invokeSuspendingFunctionWithGenericParameter() { + val method = GenericController::class.java.declaredMethods.first { it.name.startsWith("handle") } + val horse = Animal("horse") + val mono = CoroutinesUtils.invokeSuspendingFunction(method, AnimalController(), horse, null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingle()).isEqualTo(horse.name) + } + } + suspend fun suspendingFunction(value: String): String { delay(1) return value } + suspend fun suspendingFunctionWithNullable(value: String?): String? { + delay(1) + return value + } + + suspend fun suspendingFunctionWithMono(): Mono { + delay(1) + return Mono.just("foo") + } + + suspend fun suspendingFunctionWithFlux(): Flux { + delay(1) + return Flux.just("foo", "bar") + } + suspend fun suspendingFunctionWithFlow(): Flow { delay(1) return flowOf("foo", "bar") @@ -146,4 +282,78 @@ class CoroutinesUtilsTests { return value } + suspend fun suspendingUnit() { + } + + suspend fun suspendingNullable(): String? { + return null + } + + suspend fun suspendingFunctionWithValueClass(value: ValueClass): String { + delay(1) + return value.value + } + + suspend fun suspendingFunctionWithValueClassWithInit(value: ValueClassWithInit): String { + delay(1) + return value.value + } + + suspend fun suspendingFunctionWithNullableValueClass(value: ValueClass?): String? { + delay(1) + return value?.value + } + + suspend fun suspendingFunctionWithValueClassWithPrivateConstructor(value: ValueClassWithPrivateConstructor): String? { + delay(1) + return value.value + } + + suspend fun CustomException.suspendingFunctionWithExtension(): String { + delay(1) + return "${this.message}" + } + + suspend fun CustomException.suspendingFunctionWithExtensionAndParameter(limit: Int): String { + delay(1) + return "${this.message}-$limit" + } + + interface Named { + val name: String + } + + data class Animal(override val name: String) : Named + + abstract class GenericController { + + suspend fun handle(named: T): String { + delay(1) + return named.name; + } + } + + private class AnimalController : GenericController() + + @JvmInline + value class ValueClass(val value: String) + + @JvmInline + value class ValueClassWithInit(val value: String) { + init { + if (value.isEmpty()) { + throw IllegalArgumentException() + } + } + } + + @JvmInline + value class ValueClassWithPrivateConstructor private constructor(val value: String) { + companion object { + fun from(value: String) = ValueClassWithPrivateConstructor(value) + } + } + + class CustomException(message: String) : Throwable(message) + } diff --git a/spring-core/src/test/kotlin/org/springframework/core/DefaultParameterNameDiscovererKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/DefaultParameterNameDiscovererKotlinTests.kt index f5fda6eb741f..5133799db0a1 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/DefaultParameterNameDiscovererKotlinTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/DefaultParameterNameDiscovererKotlinTests.kt @@ -19,9 +19,13 @@ package org.springframework.core import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -class DefaultParameterNameDiscovererKotlinTests { - - private val parameterNameDiscoverer = DefaultParameterNameDiscoverer() +/** + * Tests for Kotlin support in [DefaultParameterNameDiscoverer]. + * + * @author Sebastien Deleuze + */ +class DefaultParameterNameDiscovererKotlinTests : + AbstractReflectionParameterNameDiscovererKotlinTests(DefaultParameterNameDiscoverer()){ enum class MyEnum { ONE, TWO @@ -31,6 +35,7 @@ class DefaultParameterNameDiscovererKotlinTests { fun getParameterNamesOnEnum() { val constructor = MyEnum::class.java.declaredConstructors[0] val actualParams = parameterNameDiscoverer.getParameterNames(constructor) - assertThat(actualParams!!.size).isEqualTo(2) + assertThat(actualParams).containsExactly("\$enum\$name", "\$enum\$ordinal") } + } diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt new file mode 100644 index 000000000000..0e5184a05d34 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.core + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test + +/** + * Kotlin tests for [KotlinDetector]. + * + * @author Sebastien Deleuze + */ +class KotlinDetectorTests { + + @Test + fun isKotlinType() { + Assertions.assertThat(KotlinDetector.isKotlinType(KotlinDetectorTests::class.java)).isTrue() + } + + @Test + fun isNotKotlinType() { + Assertions.assertThat(KotlinDetector.isKotlinType(KotlinDetector::class.java)).isFalse() + } + + @Test + fun isInlineClass() { + Assertions.assertThat(KotlinDetector.isInlineClass(ValueClass::class.java)).isTrue() + } + + @Test + fun isNotInlineClass() { + Assertions.assertThat(KotlinDetector.isInlineClass(KotlinDetector::class.java)).isFalse() + Assertions.assertThat(KotlinDetector.isInlineClass(KotlinDetectorTests::class.java)).isFalse() + } + + @JvmInline + value class ValueClass(val value: String) + +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt index 4c01bbdb916b..fb30416ef61a 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt @@ -16,48 +16,10 @@ package org.springframework.core -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -import org.springframework.util.ReflectionUtils - /** * Tests for [KotlinReflectionParameterNameDiscoverer]. + * + * @author Sebastien Deleuze */ -class KotlinReflectionParameterNameDiscovererTests { - - private val parameterNameDiscoverer = KotlinReflectionParameterNameDiscoverer() - - @Test - fun getParameterNamesOnInterface() { - val method = ReflectionUtils.findMethod(MessageService::class.java,"sendMessage", String::class.java)!! - val actualParams = parameterNameDiscoverer.getParameterNames(method) - assertThat(actualParams).contains("message") - } - - @Test - fun getParameterNamesOnClass() { - val method = ReflectionUtils.findMethod(MessageServiceImpl::class.java,"sendMessage", String::class.java)!! - val actualParams = parameterNameDiscoverer.getParameterNames(method) - assertThat(actualParams).contains("message") - } - - @Test - fun getParameterNamesOnExtensionMethod() { - val method = ReflectionUtils.findMethod(UtilityClass::class.java, "identity", String::class.java)!! - val actualParams = parameterNameDiscoverer.getParameterNames(method)!! - assertThat(actualParams).contains("\$receiver") - } - - interface MessageService { - fun sendMessage(message: String) - } - - class MessageServiceImpl { - fun sendMessage(message: String) = message - } - - class UtilityClass { - fun String.identity() = this - } -} +class KotlinReflectionParameterNameDiscovererTests : + AbstractReflectionParameterNameDiscovererKotlinTests(KotlinReflectionParameterNameDiscoverer()) diff --git a/spring-core/src/test/kotlin/org/springframework/core/MethodParameterKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/MethodParameterKotlinTests.kt index 040b294d1730..c1c8901817ea 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/MethodParameterKotlinTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/MethodParameterKotlinTests.kt @@ -38,6 +38,8 @@ class MethodParameterKotlinTests { private val nonNullableMethod = javaClass.getMethod("nonNullable", String::class.java) + private val withDefaultValueMethod: Method = javaClass.getMethod("withDefaultValue", String::class.java) + private val innerClassConstructor = InnerClass::class.java.getConstructor(MethodParameterKotlinTests::class.java) private val innerClassWithParametersConstructor = InnerClassWithParameter::class.java @@ -52,6 +54,16 @@ class MethodParameterKotlinTests { assertThat(MethodParameter(nonNullableMethod, 0).isOptional).isFalse() } + @Test + fun `Method parameter with default value`() { + assertThat(MethodParameter(withDefaultValueMethod, 0).isOptional).isTrue() + } + + @Test + fun `Method parameter without default value`() { + assertThat(MethodParameter(nonNullableMethod, 0).isOptional).isFalse() + } + @Test fun `Method return type nullability`() { assertThat(MethodParameter(nullableMethod, -1).isOptional).isTrue() @@ -123,6 +135,8 @@ class MethodParameterKotlinTests { @Suppress("unused_parameter") fun nonNullable(nonNullable: String): Int = 42 + fun withDefaultValue(withDefaultValue: String = "default") = withDefaultValue + inner class InnerClass @Suppress("unused_parameter") diff --git a/spring-core/src/test/kotlin/org/springframework/core/convert/support/DefaultConversionServiceKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/convert/support/DefaultConversionServiceKotlinTests.kt new file mode 100644 index 000000000000..d18b83f0b07c --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/convert/support/DefaultConversionServiceKotlinTests.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.support + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Tests for Kotlin support in [DefaultConversionService]. + * + * @author Stephane Nicoll + */ +class DefaultConversionServiceKotlinTests { + + private val conversionService = DefaultConversionService() + + @Test + fun stringToRegexEmptyString() { + assertThat(conversionService.convert("", Regex::class.java)).isNull(); + } + + @Test + fun stringToRegex() { + val pattern = "\\w+" + assertThat(conversionService.convert(pattern, Regex::class.java)) + .isInstanceOfSatisfying(Regex::class.java) { assertThat(it.pattern).isEqualTo(pattern) } + } + + @Test + fun regexToString() { + val pattern = "\\w+" + assertThat(conversionService.convert(pattern.toRegex(), String::class.java)).isEqualTo(pattern) + } + +} \ No newline at end of file diff --git a/spring-core/src/test/kotlin/org/springframework/core/env/PropertyResolverExtensionsKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/env/PropertyResolverExtensionsKotlinTests.kt index 776a3d6cf07c..6b8178dff7c5 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/env/PropertyResolverExtensionsKotlinTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/env/PropertyResolverExtensionsKotlinTests.kt @@ -19,7 +19,7 @@ package org.springframework.core.env import io.mockk.every import io.mockk.mockk import io.mockk.verify -import org.junit.jupiter.api.Disabled +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test /** @@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test * * @author Sebastien Deleuze */ -@Disabled class PropertyResolverExtensionsKotlinTests { val propertyResolver = mockk() @@ -46,6 +45,13 @@ class PropertyResolverExtensionsKotlinTests { verify { propertyResolver.getProperty("name", String::class.java) } } + @Test + fun `getProperty with default extension`() { + every { propertyResolver.getProperty("name", String::class.java, "default") } returns "default" + assertThat(propertyResolver.getProperty("name", "default")).isEqualTo("default") + verify { propertyResolver.getProperty("name", String::class.java, "default") } + } + @Test fun `getRequiredProperty extension`() { every { propertyResolver.getRequiredProperty("name", String::class.java) } returns "foo" diff --git a/spring-core/src/test/resources/org/springframework/core/io/support/test-resources/resource1.txt b/spring-core/src/test/resources/org/springframework/core/io/support/test-resources/resource1.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/spring-core/src/test/resources/org/springframework/core/io/support/test-resources/resource2.txt b/spring-core/src/test/resources/org/springframework/core/io/support/test-resources/resource2.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/spring-core/src/test/resources/org/springframework/core/io/support/test-resources/resource3.text b/spring-core/src/test/resources/org/springframework/core/io/support/test-resources/resource3.text new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/EnabledForTestGroups.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/EnabledForTestGroups.java index 490f42811edb..fbf32bf8015e 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/EnabledForTestGroups.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/EnabledForTestGroups.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ * @author Sam Brannen * @since 5.2 */ -@Target({ ElementType.TYPE, ElementType.METHOD }) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/EnumWithClassBody.java similarity index 87% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java rename to spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/EnumWithClassBody.java index a8064b97ace8..5797adb0a5bf 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/EnumWithClassBody.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/EnumWithClassBody.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.beans.factory.aot; +package org.springframework.core.testfixture.aot.generate.value; /** * Test enum that include a class body. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass$$GeneratedBy.java similarity index 77% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java rename to spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass$$GeneratedBy.java index 8b40ef58defd..36d070119402 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass$$GeneratedBy.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass$$GeneratedBy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.beans.factory.aot; +package org.springframework.core.testfixture.aot.generate.value; /** * Fake CGLIB generated class. * * @author Phillip Webb */ -class ExampleClass$$GeneratedBy extends ExampleClass { +public class ExampleClass$$GeneratedBy extends ExampleClass { } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass.java similarity index 84% rename from spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java rename to spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass.java index c549b9befabb..b4a5681500c5 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/ExampleClass.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generate/value/ExampleClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.beans.factory.aot; +package org.springframework.core.testfixture.aot.generate.value; /** * Public example class used for test. diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java index 97454ed162d4..961457942908 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,21 +67,21 @@ protected AbstractDecoderTests(D decoder) { * Subclasses should implement this method to test {@link Decoder#canDecode}. */ @Test - public abstract void canDecode() throws Exception; + protected abstract void canDecode() throws Exception; /** * Subclasses should implement this method to test {@link Decoder#decode}, possibly using * {@link #testDecodeAll} or other helper methods. */ @Test - public abstract void decode() throws Exception; + protected abstract void decode() throws Exception; /** * Subclasses should implement this method to test {@link Decoder#decodeToMono}, possibly using * {@link #testDecodeToMonoAll}. */ @Test - public abstract void decodeToMono() throws Exception; + protected abstract void decodeToMono() throws Exception; // Flux diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java index 2b609c9d097f..8cab9edd3e23 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,14 +67,14 @@ protected AbstractEncoderTests(E encoder) { * Subclasses should implement this method to test {@link Encoder#canEncode}. */ @Test - public abstract void canEncode() throws Exception; + protected abstract void canEncode() throws Exception; /** * Subclasses should implement this method to test {@link Encoder#encode}, possibly using * {@link #testEncodeAll} or other helper methods. */ @Test - public abstract void encode() throws Exception; + protected abstract void encode() throws Exception; /** diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java index fe37e04f88aa..4b02b677630b 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -187,7 +187,7 @@ public static void createAllocators() { } @AfterAll - public static void closeAllocators() { + static void closeAllocators() { netty5OnHeapUnpooled.close(); netty5OffHeapUnpooled.close(); netty5OnHeapPooled.close(); diff --git a/spring-expression/readme.txt b/spring-expression/readme.txt deleted file mode 100644 index d66280134a0d..000000000000 --- a/spring-expression/readme.txt +++ /dev/null @@ -1,40 +0,0 @@ -List of outstanding things to think about - turn into tickets once distilled to a core set of issues - -High Importance - -- In the resolver/executor model we cache executors. They are currently recorded in the AST and so if the user chooses to evaluate an expression -in a different context then the stored executor may be incorrect. It may harmless 'fail' which would cause us to retrieve a new one, but -can it do anything malicious? In which case we either need to forget them when the context changes or store them elsewhere. Should caching be -something that can be switched on/off by the context? (shouldCacheExecutors() on the interface?) -- Expression serialization needs supporting -- expression basic interface and common package. Should LiteralExpression be settable? should getExpressionString return quoted value? - -Low Importance - -- For the ternary operator, should isWritable() return true/false depending on evaluating the condition and check isWritable() of whichever branch it -would have taken? At the moment ternary expressions are just considered NOT writable. -- Enhance type locator interface with direct support for register/unregister imports and ability to set class loader? -- Should some of the common errors (like SpelMessages.TYPE_NOT_FOUND) be promoted to top level exceptions? -- Expression comparison - is it necessary? - -Syntax - -- should the 'is' operator change to 'instanceof' ? -- in this expression we hit the problem of not being able to write chars, since '' always means string: - evaluate("new java.lang.String('hello').charAt(2).equals('l'.charAt(0))", true, Boolean.class); - So 'l'.charAt(0) was required - wonder if we can build in a converter for a single length string to char? - Can't do that as equals take Object and so we don't know to do a cast in order to pass a char into equals - We certainly cannot do a cast (unless casts are added to the syntax). See MethodInvocationTest.testStringClass() -- MATCHES is now the thing that takes a java regex. What does 'like' do? right now it is the SQL LIKE that supports - wildcards % and _. It has a poor implementation but I need to know whether to keep it in the language before - fixing that. -- Need to agree on a standard date format for 'default' processing of dates. Currently it is: - formatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.UK); - // this is something of this format: "Wed, 4 Jul 2001 12:08:56 GMT" - // https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html -- See LiteralTests for Date (4,5,6) - should date take an expression rather than be hardcoded in the grammar - to take 2 strings only? -- when doing arithmetic, eg. 8.4 / 4 and the user asks for an Integer return type - do we silently coerce or - say we cannot as it won't fit into an int? (see OperatorTests.testMathOperatorDivide04) -- Is $index within projection/selection useful or just cute? -- All reals are represented as Doubles (so 1.25f is held internally as a double, can be converted to float when required though) - is that ok? diff --git a/spring-expression/spring-expression.gradle b/spring-expression/spring-expression.gradle index 920f7c7bc07f..4b31d5b6f5b8 100644 --- a/spring-expression/spring-expression.gradle +++ b/spring-expression/spring-expression.gradle @@ -4,6 +4,7 @@ apply plugin: "kotlin" dependencies { api(project(":spring-core")) + optional("org.jetbrains.kotlin:kotlin-reflect") testImplementation(testFixtures(project(":spring-core"))) testImplementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java b/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java index 7530c36f6ee3..f6fa6e37f16a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +19,18 @@ /** * A bean resolver can be registered with the evaluation context and will kick in * for bean references: {@code @myBeanName} and {@code &myBeanName} expressions. - * The {@code &} variant syntax allows access to the factory bean where relevant. + * + *

      The {@code &} variant syntax allows access to the factory bean where relevant. * * @author Andy Clement * @since 3.0.3 */ +@FunctionalInterface public interface BeanResolver { /** * Look up a bean by the given name and return a corresponding instance for it. - * For attempting access to a factory bean, the name needs a {@code &} prefix. + *

      For attempting access to a factory bean, the name needs a {@code &} prefix. * @param context the current evaluation context * @param beanName the name of the bean to look up * @return an object representing the bean diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java index c7e0fd355c3c..363506b951f6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,32 +16,38 @@ package org.springframework.expression; - -// TODO Is the resolver/executor model too pervasive in this package? /** - * Executors are built by resolvers and can be cached by the infrastructure to repeat an - * operation quickly without going back to the resolvers. For example, the particular - * constructor to run on a class may be discovered by the reflection constructor resolver - * - it will then build a ConstructorExecutor that executes that constructor and the - * ConstructorExecutor can be reused without needing to go back to the resolver to - * discover the constructor again. + * A {@code ConstructorExecutor} is built by a {@link ConstructorResolver} and + * can be cached by the infrastructure to repeat an operation quickly without + * going back to the resolvers. + * + *

      For example, the particular constructor to execute on a class may be discovered + * by a {@code ConstructorResolver} which then builds a {@code ConstructorExecutor} + * that executes that constructor, and the resolved {@code ConstructorExecutor} + * can be reused without needing to go back to the resolvers to discover the + * constructor again. * - *

      They can become stale, and in that case should throw an AccessException - this will - * cause the infrastructure to go back to the resolvers to ask for a new one. + *

      If a {@code ConstructorExecutor} becomes stale, it should throw an + * {@link AccessException} which signals to the infrastructure to go back to the + * resolvers to ask for a new one. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see ConstructorResolver + * @see MethodExecutor */ +@FunctionalInterface public interface ConstructorExecutor { /** * Execute a constructor in the specified context using the specified arguments. - * @param context the evaluation context in which the command is being executed - * @param arguments the arguments to the constructor call, should match (in terms - * of number and type) whatever the command will need to run + * @param context the evaluation context in which the constructor is being executed + * @param arguments the arguments to the constructor; should match (in terms + * of number and type) whatever the constructor will need to run * @return the new object - * @throws AccessException if there is a problem executing the command or the - * CommandExecutor is no longer valid + * @throws AccessException if there is a problem executing the constructor or + * if this {@code ConstructorExecutor} has become stale */ TypedValue execute(EvaluationContext context, Object... arguments) throws AccessException; diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java index 821a46c1bd20..3c9c232c3ee1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,24 +22,33 @@ import org.springframework.lang.Nullable; /** - * A constructor resolver attempts to locate a constructor and returns a ConstructorExecutor - * that can be used to invoke that constructor. The ConstructorExecutor will be cached but - * if it 'goes stale' the resolvers will be called again. + * A constructor resolver attempts to locate a constructor and returns a + * {@link ConstructorExecutor} that can be used to invoke that constructor. + * + *

      The {@code ConstructorExecutor} will be cached, but if it becomes stale the + * resolvers will be called again. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see ConstructorExecutor + * @see MethodResolver */ @FunctionalInterface public interface ConstructorResolver { /** - * Within the supplied context determine a suitable constructor on the supplied type - * that can handle the specified arguments. Return a ConstructorExecutor that can be - * used to invoke that constructor (or {@code null} if no constructor could be found). + * Within the supplied context, resolve a suitable constructor on the + * supplied type that can handle the specified arguments. + *

      Returns a {@link ConstructorExecutor} that can be used to invoke that + * constructor (or {@code null} if no constructor could be found). * @param context the current evaluation context - * @param typeName the type upon which to look for the constructor - * @param argumentTypes the arguments that the constructor must be able to handle - * @return a ConstructorExecutor that can invoke the constructor, or null if non found + * @param typeName the fully-qualified name of the type upon which to look + * for the constructor + * @param argumentTypes the types of arguments that the constructor must be + * able to handle + * @return a {@code ConstructorExecutor} that can invoke the constructor, + * or {@code null} if the constructor cannot be found */ @Nullable ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index 0c393a86dbe6..598c461df3eb 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,6 +121,7 @@ default TypedValue assignVariable(String name, Supplier valueSupplie * configuration for the context. * @param name the name of the variable to set * @param value the value to be placed in the variable + * @see #lookupVariable(String) */ void setVariable(String name, @Nullable Object value); @@ -132,4 +133,18 @@ default TypedValue assignVariable(String name, Supplier valueSupplie @Nullable Object lookupVariable(String name); + /** + * Determine if assignment is enabled within expressions evaluated by this evaluation + * context. + *

      If this method returns {@code false}, the assignment ({@code =}), increment + * ({@code ++}), and decrement ({@code --}) operators are disabled. + *

      By default, this method returns {@code true}. Concrete implementations may override + * this default method to disable assignment. + * @return {@code true} if assignment is enabled; {@code false} otherwise + * @since 5.3.38 + */ + default boolean isAssignmentEnabled() { + return true; + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java index 400f561c0e84..ec66a5268294 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java @@ -16,6 +16,8 @@ package org.springframework.expression; +import org.springframework.lang.Nullable; + /** * Represent an exception that occurs during expression evaluation. * @@ -38,7 +40,7 @@ public EvaluationException(String message) { * @param message description of the problem that occurred * @param cause the underlying cause of this exception */ - public EvaluationException(String message, Throwable cause) { + public EvaluationException(String message, @Nullable Throwable cause) { super(message,cause); } @@ -66,7 +68,7 @@ public EvaluationException(String expressionString, String message) { * @param message description of the problem that occurred * @param cause the underlying cause of this exception */ - public EvaluationException(int position, String message, Throwable cause) { + public EvaluationException(int position, String message, @Nullable Throwable cause) { super(position, message, cause); } diff --git a/spring-expression/src/main/java/org/springframework/expression/Expression.java b/spring-expression/src/main/java/org/springframework/expression/Expression.java index 8babd78243f1..97e89ab671e0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/Expression.java +++ b/spring-expression/src/main/java/org/springframework/expression/Expression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,10 @@ /** * An expression capable of evaluating itself against context objects. - * Encapsulates the details of a previously parsed expression string. - * Provides a common abstraction for expression evaluation. + * + *

      Encapsulates the details of a previously parsed expression string. + * + *

      Provides a common abstraction for expression evaluation. * * @author Keith Donald * @author Andy Clement @@ -46,10 +48,10 @@ public interface Expression { Object getValue() throws EvaluationException; /** - * Evaluate the expression in the default context. If the result + * Evaluate this expression in the default context. If the result * of the evaluation does not match (and cannot be converted to) - * the expected result type then an exception will be returned. - * @param desiredResultType the class the caller would like the result to be + * the expected result type then an exception will be thrown. + * @param desiredResultType the type the caller would like the result to be * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ @@ -66,16 +68,17 @@ public interface Expression { Object getValue(@Nullable Object rootObject) throws EvaluationException; /** - * Evaluate the expression in the default context against the specified root + * Evaluate this expression in the default context against the specified root * object. If the result of the evaluation does not match (and cannot be - * converted to) the expected result type then an exception will be returned. + * converted to) the expected result type then an exception will be thrown. * @param rootObject the root object against which to evaluate the expression - * @param desiredResultType the class the caller would like the result to be + * @param desiredResultType the type the caller would like the result to be * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ @Nullable - T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException; + T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) + throws EvaluationException; /** * Evaluate this expression in the provided context and return the result @@ -100,27 +103,28 @@ public interface Expression { Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; /** - * Evaluate the expression in a specified context which can resolve references + * Evaluate this expression in the provided context which can resolve references * to properties, methods, types, etc. The type of the evaluation result is - * expected to be of a particular class and an exception will be thrown if it + * expected to be of a particular type, and an exception will be thrown if it * is not and cannot be converted to that type. * @param context the context in which to evaluate the expression - * @param desiredResultType the class the caller would like the result to be + * @param desiredResultType the type the caller would like the result to be * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ @Nullable - T getValue(EvaluationContext context, @Nullable Class desiredResultType) throws EvaluationException; + T getValue(EvaluationContext context, @Nullable Class desiredResultType) + throws EvaluationException; /** - * Evaluate the expression in a specified context which can resolve references + * Evaluate this expression in the provided context which can resolve references * to properties, methods, types, etc. The type of the evaluation result is - * expected to be of a particular class and an exception will be thrown if it - * is not and cannot be converted to that type. The supplied root object - * overrides any default specified on the supplied context. + * expected to be of a particular type, and an exception will be thrown if it + * is not and cannot be converted to that type.j + *

      The supplied root object overrides any specified in the supplied context. * @param context the context in which to evaluate the expression * @param rootObject the root object against which to evaluate the expression - * @param desiredResultType the class the caller would like the result to be + * @param desiredResultType the type the caller would like the result to be * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ @@ -129,9 +133,9 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable throws EvaluationException; /** - * Return the most general type that can be passed to a {@link #setValue} - * method using the default context. - * @return the most general type of value that can be set on this context + * Return the most general type that can be passed to the + * {@link #setValue(EvaluationContext, Object)} method using the default context. + * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable @@ -141,7 +145,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * Return the most general type that can be passed to the * {@link #setValue(Object, Object)} method using the default context. * @param rootObject the root object against which to evaluate the expression - * @return the most general type of value that can be set on this context + * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable @@ -151,7 +155,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * Return the most general type that can be passed to the * {@link #setValue(EvaluationContext, Object)} method for the given context. * @param context the context in which to evaluate the expression - * @return the most general type of value that can be set on this context + * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable @@ -160,58 +164,61 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable /** * Return the most general type that can be passed to the * {@link #setValue(EvaluationContext, Object, Object)} method for the given - * context. The supplied root object overrides any specified in the context. + * context. + *

      The supplied root object overrides any specified in the supplied context. * @param context the context in which to evaluate the expression * @param rootObject the root object against which to evaluate the expression - * @return the most general type of value that can be set on this context + * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; /** - * Return the most general type that can be passed to a {@link #setValue} - * method using the default context. - * @return a type descriptor for values that can be set on this context + * Return a descriptor for the most general type that can be passed to one of + * the {@code setValue(...)} methods using the default context. + * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable TypeDescriptor getValueTypeDescriptor() throws EvaluationException; /** - * Return the most general type that can be passed to the + * Return a descriptor for the most general type that can be passed to the * {@link #setValue(Object, Object)} method using the default context. * @param rootObject the root object against which to evaluate the expression - * @return a type descriptor for values that can be set on this context + * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException; /** - * Return the most general type that can be passed to the + * Return a descriptor for the most general type that can be passed to the * {@link #setValue(EvaluationContext, Object)} method for the given context. * @param context the context in which to evaluate the expression - * @return a type descriptor for values that can be set on this context + * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException; /** - * Return the most general type that can be passed to the + * Return a descriptor for the most general type that can be passed to the * {@link #setValue(EvaluationContext, Object, Object)} method for the given - * context. The supplied root object overrides any specified in the context. + * context. + *

      The supplied root object overrides any specified in the supplied context. * @param context the context in which to evaluate the expression * @param rootObject the root object against which to evaluate the expression - * @return a type descriptor for values that can be set on this context + * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ @Nullable - TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; + TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) + throws EvaluationException; /** - * Determine if an expression can be written to, i.e. setValue() can be called. + * Determine if this expression can be written to, i.e. setValue() can be called. * @param rootObject the root object against which to evaluate the expression * @return {@code true} if the expression is writable; {@code false} otherwise * @throws EvaluationException if there is a problem determining if it is writable @@ -219,7 +226,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable boolean isWritable(@Nullable Object rootObject) throws EvaluationException; /** - * Determine if an expression can be written to, i.e. setValue() can be called. + * Determine if this expression can be written to, i.e. setValue() can be called. * @param context the context in which the expression should be checked * @return {@code true} if the expression is writable; {@code false} otherwise * @throws EvaluationException if there is a problem determining if it is writable @@ -227,8 +234,8 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable boolean isWritable(EvaluationContext context) throws EvaluationException; /** - * Determine if an expression can be written to, i.e. setValue() can be called. - * The supplied root object overrides any specified in the context. + * Determine if this expression can be written to, i.e. setValue() can be called. + *

      The supplied root object overrides any specified in the supplied context. * @param context the context in which the expression should be checked * @param rootObject the root object against which to evaluate the expression * @return {@code true} if the expression is writable; {@code false} otherwise @@ -254,12 +261,13 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable /** * Set this expression in the provided context to the value provided. - * The supplied root object overrides any specified in the context. + *

      The supplied root object overrides any specified in the supplied context. * @param context the context in which to set the value of the expression * @param rootObject the root object against which to evaluate the expression * @param value the new value * @throws EvaluationException if there is a problem during evaluation */ - void setValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) throws EvaluationException; + void setValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) + throws EvaluationException; } diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java index 4fb1fe8b4225..0b63adb694c6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java @@ -49,7 +49,7 @@ public ExpressionException(String message) { * @param message a descriptive message * @param cause the underlying cause of this exception */ - public ExpressionException(String message, Throwable cause) { + public ExpressionException(String message, @Nullable Throwable cause) { super(message, cause); this.expressionString = null; this.position = 0; @@ -95,7 +95,7 @@ public ExpressionException(int position, String message) { * @param message a descriptive message * @param cause the underlying cause of this exception */ - public ExpressionException(int position, String message, Throwable cause) { + public ExpressionException(int position, String message, @Nullable Throwable cause) { super(message, cause); this.expressionString = null; this.position = position; @@ -123,7 +123,6 @@ public final int getPosition() { * @see #getSimpleMessage() * @see java.lang.Throwable#getMessage() */ - @Override public String getMessage() { return toDetailedString(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java index 9753ef898578..1cb24e5d1672 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java @@ -16,6 +16,8 @@ package org.springframework.expression; +import org.springframework.lang.Nullable; + /** * This exception wraps (as cause) a checked exception thrown by some method that SpEL * invokes. It differs from a SpelEvaluationException because this indicates the @@ -28,7 +30,7 @@ @SuppressWarnings("serial") public class ExpressionInvocationTargetException extends EvaluationException { - public ExpressionInvocationTargetException(int position, String message, Throwable cause) { + public ExpressionInvocationTargetException(int position, String message, @Nullable Throwable cause) { super(position, message, cause); } @@ -40,7 +42,7 @@ public ExpressionInvocationTargetException(String expressionString, String messa super(expressionString, message); } - public ExpressionInvocationTargetException(String message, Throwable cause) { + public ExpressionInvocationTargetException(String message, @Nullable Throwable cause) { super(message, cause); } diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java index 3c0b44d9d9b5..6d717cd4b008 100644 --- a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,30 +17,38 @@ package org.springframework.expression; /** - * MethodExecutors are built by the resolvers and can be cached by the infrastructure to - * repeat an operation quickly without going back to the resolvers. For example, the - * particular method to run on an object may be discovered by the reflection method - * resolver - it will then build a MethodExecutor that executes that method and the - * MethodExecutor can be reused without needing to go back to the resolver to discover - * the method again. + * A {@code MethodExecutor} is built by a {@link MethodResolver} and can be cached + * by the infrastructure to repeat an operation quickly without going back to the + * resolvers. * - *

      They can become stale, and in that case should throw an AccessException: - * This will cause the infrastructure to go back to the resolvers to ask for a new one. + *

      For example, the particular method to execute on an object may be discovered + * by a {@code MethodResolver} which then builds a {@code MethodExecutor} that + * executes that method, and the resolved {@code MethodExecutor} can be reused + * without needing to go back to the resolvers to discover the method again. + * + *

      If a {@code MethodExecutor} becomes stale, it should throw an + * {@link AccessException} which signals to the infrastructure to go back to the + * resolvers to ask for a new one. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see MethodResolver + * @see ConstructorExecutor */ +@FunctionalInterface public interface MethodExecutor { /** - * Execute a command using the specified arguments, and using the specified expression state. - * @param context the evaluation context in which the command is being executed - * @param target the target object of the call - null for static methods - * @param arguments the arguments to the executor, should match (in terms of number - * and type) whatever the command will need to run - * @return the value returned from execution - * @throws AccessException if there is a problem executing the command or the - * MethodExecutor is no longer valid + * Execute a method in the specified context using the specified arguments. + * @param context the evaluation context in which the method is being executed + * @param target the target of the method invocation; may be {@code null} for + * {@code static} methods + * @param arguments the arguments to the method; should match (in terms of + * number and type) whatever the method will need to run + * @return the value returned from the method + * @throws AccessException if there is a problem executing the method or + * if this {@code MethodExecutor} has become stale */ TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException; diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java index d4216d2a55c3..9fb9137c8e4e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,23 +22,33 @@ import org.springframework.lang.Nullable; /** - * A method resolver attempts to locate a method and returns a command executor that can be - * used to invoke that method. The command executor will be cached, but if it 'goes stale' - * the resolvers will be called again. + * A method resolver attempts to locate a method and returns a + * {@link MethodExecutor} that can be used to invoke that method. + * + *

      The {@code MethodExecutor} will be cached, but if it becomes stale the + * resolvers will be called again. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 + * @see MethodExecutor + * @see ConstructorResolver */ +@FunctionalInterface public interface MethodResolver { /** - * Within the supplied context determine a suitable method on the supplied object that - * can handle the specified arguments. Return a {@link MethodExecutor} that can be used - * to invoke that method, or {@code null} if no method could be found. + * Within the supplied context, resolve a suitable method on the supplied + * object that can handle the specified arguments. + *

      Returns a {@link MethodExecutor} that can be used to invoke that method, + * or {@code null} if no method could be found. * @param context the current evaluation context * @param targetObject the object upon which the method is being called - * @param argumentTypes the arguments that the constructor must be able to handle - * @return a MethodExecutor that can invoke the method, or null if the method cannot be found + * @param name the name of the method + * @param argumentTypes the types of arguments that the method must be able + * to handle + * @return a {@code MethodExecutor} that can invoke the method, or {@code null} + * if the method cannot be found */ @Nullable MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, diff --git a/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java b/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java index 2e6d431ed85d..8bece1f0fefc 100644 --- a/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java +++ b/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,9 @@ import org.springframework.lang.Nullable; /** - * By default the mathematical operators {@link Operation} support simple types - * like numbers. By providing an implementation of OperatorOverloader, a user - * of the expression language can support these operations on other types. + * By default, the mathematical operators defined in {@link Operation} support simple + * types like numbers. By providing an implementation of {@code OperatorOverloader}, + * a user of the expression language can support these operations on other types. * * @author Andy Clement * @since 3.0 @@ -29,21 +29,21 @@ public interface OperatorOverloader { /** - * Return true if the operator overloader supports the specified operation - * between the two operands and so should be invoked to handle it. + * Return {@code true} if this operator overloader supports the specified + * operation on the two operands and should be invoked to handle it. * @param operation the operation to be performed * @param leftOperand the left operand * @param rightOperand the right operand - * @return true if the OperatorOverloader supports the specified operation - * between the two operands + * @return true if this {@code OperatorOverloader} supports the specified + * operation between the two operands * @throws EvaluationException if there is a problem performing the operation */ boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) throws EvaluationException; /** - * Execute the specified operation on two operands, returning a result. - * See {@link Operation} for supported operations. + * Perform the specified operation on the two operands, returning a result. + *

      See {@link Operation} for supported operations. * @param operation the operation to be performed * @param leftOperand the left operand * @param rightOperand the right operand diff --git a/spring-expression/src/main/java/org/springframework/expression/TypedValue.java b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java index ccd8b5723524..b97020a259ec 100644 --- a/spring-expression/src/main/java/org/springframework/expression/TypedValue.java +++ b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,8 @@ /** * Encapsulates an object and a {@link TypeDescriptor} that describes it. - * The type descriptor can contain generic declarations that would not + * + *

      The type descriptor can contain generic declarations that would not * be accessible through a simple {@code getClass()} call on the object. * * @author Andy Clement diff --git a/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java index a37cb2162eb8..8bb6dcf195ce 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,17 +24,19 @@ import org.springframework.lang.Nullable; /** - * Represents a template expression broken into pieces. Each piece will be an Expression - * but pure text parts to the template will be represented as LiteralExpression objects. - * An example of a template expression might be: + * Represents a template expression broken into pieces. + * + *

      Each piece will be an {@link Expression}, but pure text parts of the + * template will be represented as {@link LiteralExpression} objects. An example + * of a template expression might be: * *

        * "Hello ${getName()}"
        * 
      * - * which will be represented as a CompositeStringExpression of two parts. The first part - * being a LiteralExpression representing 'Hello ' and the second part being a real - * expression that will call {@code getName()} when invoked. + * which will be represented as a {@code CompositeStringExpression} of two parts: + * the first part being a {@link LiteralExpression} representing 'Hello ' and the + * second part being a real expression that will call {@code getName()} when invoked. * * @author Andy Clement * @author Juergen Hoeller @@ -78,7 +80,7 @@ public String getValue() throws EvaluationException { @Override @Nullable public T getValue(@Nullable Class expectedResultType) throws EvaluationException { - Object value = getValue(); + String value = getValue(); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); } @@ -97,7 +99,7 @@ public String getValue(@Nullable Object rootObject) throws EvaluationException { @Override @Nullable public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(rootObject); + String value = getValue(rootObject); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); } @@ -118,7 +120,7 @@ public String getValue(EvaluationContext context) throws EvaluationException { public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { - Object value = getValue(context); + String value = getValue(context); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); } @@ -139,7 +141,7 @@ public String getValue(EvaluationContext context, @Nullable Object rootObject) t public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(context,rootObject); + String value = getValue(context,rootObject); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); } diff --git a/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java index 07c64bd9f615..ca9275ad214c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,11 @@ import org.springframework.lang.Nullable; /** - * A very simple hardcoded implementation of the Expression interface that represents a - * string literal. It is used with CompositeStringExpression when representing a template - * expression which is made up of pieces - some being real expressions to be handled by + * A very simple, hard-coded implementation of the {@link Expression} interface + * that represents a string literal. + * + *

      It is used with {@link CompositeStringExpression} when representing a template + * expression which is made up of pieces, some being real expressions to be handled by * an EL implementation like SpEL, and some being just textual elements. * * @author Andy Clement @@ -62,7 +64,7 @@ public String getValue() { @Override @Nullable public T getValue(@Nullable Class expectedResultType) throws EvaluationException { - Object value = getValue(); + String value = getValue(); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); } @@ -74,7 +76,7 @@ public String getValue(@Nullable Object rootObject) { @Override @Nullable public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(rootObject); + String value = getValue(rootObject); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); } @@ -85,10 +87,8 @@ public String getValue(EvaluationContext context) { @Override @Nullable - public T getValue(EvaluationContext context, @Nullable Class expectedResultType) - throws EvaluationException { - - Object value = getValue(context); + public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { + String value = getValue(context); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); } @@ -102,7 +102,7 @@ public String getValue(EvaluationContext context, @Nullable Object rootObject) t public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(context, rootObject); + String value = getValue(context, rootObject); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); } diff --git a/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java index 0f5ccbddbd27..f979a258fb49 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,11 +80,11 @@ private Expression parseTemplate(String expressionString, ParserContext context) * result, evaluating all returned expressions and concatenating the results produces * the complete evaluated string. Unwrapping is only done of the outermost delimiters * found, so the string 'hello ${foo${abc}}' would break into the pieces 'hello ' and - * 'foo${abc}'. This means that expression languages that used ${..} as part of their + * 'foo${abc}'. This means that expression languages that use ${..} as part of their * functionality are supported without any problem. The parsing is aware of the * structure of an embedded expression. It assumes that parentheses '(', square - * brackets '[' and curly brackets '}' must be in pairs within the expression unless - * they are within a string literal and a string literal starts and terminates with a + * brackets '[', and curly brackets '}' must be in pairs within the expression unless + * they are within a string literal and the string literal starts and terminates with a * single quote '. * @param expressionString the expression string * @return the parsed expressions @@ -184,14 +184,10 @@ private int skipToCorrectEndSuffix(String suffix, String expressionString, int a } char ch = expressionString.charAt(pos); switch (ch) { - case '{': - case '[': - case '(': + case '{', '[', '(' -> { stack.push(new Bracket(ch, pos)); - break; - case '}': - case ']': - case ')': + } + case '}', ']', ')' -> { if (stack.isEmpty()) { throw new ParseException(expressionString, pos, "Found closing '" + ch + "' at position " + pos + " without an opening '" + @@ -203,9 +199,8 @@ private int skipToCorrectEndSuffix(String suffix, String expressionString, int a "' at position " + pos + " but most recent opening is '" + p.bracket + "' at position " + p.pos); } - break; - case '\'': - case '"': + } + case '\'', '"' -> { // jump to the end of the literal int endLiteral = expressionString.indexOf(ch, pos + 1); if (endLiteral == -1) { @@ -213,7 +208,7 @@ private int skipToCorrectEndSuffix(String suffix, String expressionString, int a "Found non terminating string literal starting at position " + pos); } pos = endLiteral; - break; + } } pos++; } @@ -243,48 +238,33 @@ protected abstract Expression doParseExpression(String expressionString, @Nullab /** * This captures a type of bracket and the position in which it occurs in the * expression. The positional information is used if an error has to be reported - * because the related end bracket cannot be found. Bracket is used to describe: - * square brackets [] round brackets () and curly brackets {} + * because the related end bracket cannot be found. Bracket is used to describe + * square brackets [], round brackets (), and curly brackets {}. */ - private static class Bracket { - - char bracket; - - int pos; - - Bracket(char bracket, int pos) { - this.bracket = bracket; - this.pos = pos; - } + private record Bracket(char bracket, int pos) { boolean compatibleWithCloseBracket(char closeBracket) { - if (this.bracket == '{') { - return closeBracket == '}'; - } - else if (this.bracket == '[') { - return closeBracket == ']'; - } - return closeBracket == ')'; + return switch (this.bracket) { + case '{' -> closeBracket == '}'; + case '[' -> closeBracket == ']'; + default -> closeBracket == ')'; + }; } static char theOpenBracketFor(char closeBracket) { - if (closeBracket == '}') { - return '{'; - } - else if (closeBracket == ']') { - return '['; - } - return '('; + return switch (closeBracket) { + case '}' -> '{'; + case ']' -> '['; + default -> '('; + }; } static char theCloseBracketFor(char openBracket) { - if (openBracket == '{') { - return '}'; - } - else if (openBracket == '[') { - return ']'; - } - return ')'; + return switch (openBracket) { + case '{' -> '}'; + case '[' -> ']'; + default -> ')'; + }; } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java index f9e2fb11b48f..e6b57a31355c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,7 +99,7 @@ public CodeFlow(String className, ClassWriter classWriter) { this.className = className; this.classWriter = classWriter; this.compilationScopes = new ArrayDeque<>(); - this.compilationScopes.add(new ArrayList()); + this.compilationScopes.add(new ArrayList<>()); } @@ -450,35 +450,35 @@ public static String toJvmDescriptor(Class clazz) { if (clazz.isArray()) { while (clazz.isArray()) { sb.append('['); - clazz = clazz.getComponentType(); + clazz = clazz.componentType(); } } if (clazz.isPrimitive()) { - if (clazz == Boolean.TYPE) { + if (clazz == boolean.class) { sb.append('Z'); } - else if (clazz == Byte.TYPE) { + else if (clazz == byte.class) { sb.append('B'); } - else if (clazz == Character.TYPE) { + else if (clazz == char.class) { sb.append('C'); } - else if (clazz == Double.TYPE) { + else if (clazz == double.class) { sb.append('D'); } - else if (clazz == Float.TYPE) { + else if (clazz == float.class) { sb.append('F'); } - else if (clazz == Integer.TYPE) { + else if (clazz == int.class) { sb.append('I'); } - else if (clazz == Long.TYPE) { + else if (clazz == long.class) { sb.append('J'); } - else if (clazz == Short.TYPE) { + else if (clazz == short.class) { sb.append('S'); } - else if (clazz == Void.TYPE) { + else if (clazz == void.class) { sb.append('V'); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java index 634267d75bc2..6651bd688878 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import org.springframework.expression.PropertyAccessor; /** - * A compilable property accessor is able to generate bytecode that represents + * A compilable {@link PropertyAccessor} is able to generate bytecode that represents * the access operation, facilitating compilation to bytecode of expressions * that use the accessor. * @@ -41,12 +41,13 @@ public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes { Class getPropertyType(); /** - * Generate the bytecode the performs the access operation into the specified MethodVisitor - * using context information from the codeflow where necessary. + * Generate the bytecode the performs the access operation into the specified + * {@link MethodVisitor} using context information from the {@link CodeFlow} + * where necessary. * @param propertyName the name of the property - * @param mv the Asm method visitor into which code should be generated - * @param cf the current state of the expression compiler + * @param methodVisitor the ASM method visitor into which code should be generated + * @param codeFlow the current state of the expression compiler */ - void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf); + void generateCode(String propertyName, MethodVisitor methodVisitor, CodeFlow codeFlow); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 8dadae596732..338b18dad3e3 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -93,6 +93,7 @@ public ExpressionState(EvaluationContext context, TypedValue rootObject) { public ExpressionState(EvaluationContext context, TypedValue rootObject, SpelParserConfiguration configuration) { Assert.notNull(context, "EvaluationContext must not be null"); + Assert.notNull(rootObject, "'rootObject' must not be null"); Assert.notNull(configuration, "SpelParserConfiguration must not be null"); this.relatedContext = context; this.rootObject = rootObject; @@ -203,7 +204,7 @@ public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor /* * A new scope is entered when a function is invoked. */ - public void enterScope(Map argMap) { + public void enterScope(@Nullable Map argMap) { initVariableScopes().push(new VariableScope(argMap)); initScopeRootObjects().push(getActiveContextObject()); } @@ -300,9 +301,10 @@ public VariableScope(@Nullable Map arguments) { } public VariableScope(String name, Object value) { - this.vars.put(name,value); + this.vars.put(name, value); } + @Nullable public Object lookupVariable(String name) { return this.vars.get(name); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java b/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java index 3debd82a46d0..d6e173c5f140 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java @@ -16,6 +16,8 @@ package org.springframework.expression.spel; +import org.springframework.lang.Nullable; + /** * Wraps a real parse exception. This exception flows to the top parse method and then * the wrapped exception is thrown as the real problem. @@ -26,11 +28,12 @@ @SuppressWarnings("serial") public class InternalParseException extends RuntimeException { - public InternalParseException(SpelParseException cause) { + public InternalParseException(@Nullable SpelParseException cause) { super(cause); } @Override + @Nullable public SpelParseException getCause() { return (SpelParseException) super.getCause(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java index c55fe0b6e3bf..9817ba413490 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,15 @@ package org.springframework.expression.spel; import org.springframework.expression.EvaluationException; +import org.springframework.lang.Nullable; /** - * Root exception for Spring EL related exceptions. Rather than holding a hard coded - * string indicating the problem, it records a message key and the inserts for the - * message. See {@link SpelMessage} for the list of all possible messages that can occur. + * Root exception for Spring EL related exceptions. + * + *

      Rather than holding a hard-coded string indicating the problem, it records + * a message key and the inserts for the message. + * + *

      See {@link SpelMessage} for the list of all possible messages that can occur. * * @author Andy Clement * @author Juergen Hoeller @@ -32,28 +36,29 @@ public class SpelEvaluationException extends EvaluationException { private final SpelMessage message; + @Nullable private final Object[] inserts; - public SpelEvaluationException(SpelMessage message, Object... inserts) { + public SpelEvaluationException(SpelMessage message, @Nullable Object... inserts) { super(message.formatMessage(inserts)); this.message = message; this.inserts = inserts; } - public SpelEvaluationException(int position, SpelMessage message, Object... inserts) { + public SpelEvaluationException(int position, SpelMessage message, @Nullable Object... inserts) { super(position, message.formatMessage(inserts)); this.message = message; this.inserts = inserts; } - public SpelEvaluationException(int position, Throwable cause, SpelMessage message, Object... inserts) { + public SpelEvaluationException(int position, @Nullable Throwable cause, SpelMessage message, @Nullable Object... inserts) { super(position, message.formatMessage(inserts), cause); this.message = message; this.inserts = inserts; } - public SpelEvaluationException(Throwable cause, SpelMessage message, Object... inserts) { + public SpelEvaluationException(@Nullable Throwable cause, SpelMessage message, @Nullable Object... inserts) { super(message.formatMessage(inserts), cause); this.message = message; this.inserts = inserts; @@ -77,6 +82,7 @@ public SpelMessage getMessageCode() { /** * Return the message inserts. */ + @Nullable public Object[] getInserts() { return this.inserts; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index d9735071d1fd..2356b6011c59 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,12 @@ import java.text.MessageFormat; +import org.springframework.lang.Nullable; + /** * Contains all the messages that can be produced by the Spring Expression Language. - * Each message has a kind (info, warn, error) and a code number. Tests can be written to + * + *

      Each message has a kind (info, warn, error) and a code number. Tests can be written to * expect particular code numbers rather than particular text, enabling the message text * to more easily be modified and the tests to run successfully in different locales. * @@ -76,7 +79,7 @@ public enum SpelMessage { "Cannot compare instances of {0} and {1}"), INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION(Kind.ERROR, 1014, - "Incorrect number of arguments for function, {0} supplied but function takes {1}"), + "Incorrect number of arguments for function ''{0}'': {1} supplied but function takes {2}"), INVALID_TYPE_FOR_SELECTION(Kind.ERROR, 1015, "Cannot perform selection on input data of type ''{0}''"), @@ -100,7 +103,7 @@ public enum SpelMessage { "A problem occurred whilst attempting to access the property ''{0}'': ''{1}''"), FUNCTION_REFERENCE_CANNOT_BE_INVOKED(Kind.ERROR, 1022, - "The function ''{0}'' mapped to an object of type ''{1}'' which cannot be invoked"), + "The function ''{0}'' mapped to an object of type ''{1}'' cannot be invoked"), EXCEPTION_DURING_FUNCTION_CALL(Kind.ERROR, 1023, "A problem occurred whilst attempting to invoke the function ''{0}'': ''{1}''"), @@ -129,7 +132,7 @@ public enum SpelMessage { PROBLEM_LOCATING_METHOD(Kind.ERROR, 1031, "Problem locating method {0} on type {1}"), - SETVALUE_NOT_SUPPORTED( Kind.ERROR, 1032, + SETVALUE_NOT_SUPPORTED(Kind.ERROR, 1032, "setValue(ExpressionState, Object) not supported for ''{0}''"), MULTIPLE_POSSIBLE_METHODS(Kind.ERROR, 1033, @@ -312,7 +315,7 @@ public enum SpelMessage { * @return a formatted message * @since 4.3.5 */ - public String formatMessage(Object... inserts) { + public String formatMessage(@Nullable Object... inserts) { StringBuilder formattedMessage = new StringBuilder(); formattedMessage.append("EL").append(this.code); if (this.kind == Kind.ERROR) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java index ff17068fbbc4..f0430ac2d676 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public class SpelParserConfiguration { * Default maximum length permitted for a SpEL expression. * @since 5.2.24 */ - private static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000; + public static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000; /** System property to configure the default compiler mode for SpEL expression parsers: {@value}. */ public static final String SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME = "spring.expression.compiler.mode"; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java index 55e5d2e4ff08..1b47ead1607f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; /** * Represents assignment. An alternative to calling {@code setValue} @@ -39,6 +41,9 @@ public Assign(int startPos, int endPos, SpelNodeImpl... operands) { @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST()); + } return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state)); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java index 8a540a908dc6..593e5164b0ec 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; -import org.springframework.expression.spel.SpelNode; /** * Represents a DOT separated expression sequence, such as @@ -120,14 +119,13 @@ public String toStringAST() { for (int i = 0; i < getChildCount(); i++) { sb.append(getChild(i).toStringAST()); if (i < getChildCount() - 1) { - SpelNode nextChild = getChild(i + 1); + SpelNodeImpl nextChild = this.children[i + 1]; + if (nextChild.isNullSafe()) { + sb.append("?."); + } // Don't append a '.' if the next child is an Indexer. // For example, we want 'myVar[0]' instead of 'myVar.[0]'. - if (!(nextChild instanceof Indexer)) { - if ((nextChild instanceof MethodReference methodRef && methodRef.isNullSafe()) || - (nextChild instanceof PropertyOrFieldReference pofRef && pofRef.isNullSafe())) { - sb.append('?'); - } + else if (!(nextChild instanceof Indexer)) { sb.append('.'); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java index 7b3a73596881..d5f071820cd1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -230,7 +230,7 @@ public String toStringAST() { InlineList initializer = (InlineList) getChild(1); sb.append("[] ").append(initializer.toStringAST()); } - else { + else if (this.dimensions != null) { // new int[3], new java.lang.String[3][4], etc. for (SpelNodeImpl dimension : this.dimensions) { sb.append('[').append(dimension.toStringAST()).append(']'); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java index 55e2267c4ce2..6f6c26f8f29e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,9 @@ import org.springframework.util.ObjectUtils; /** - * Represents the elvis operator ?:. For an expression a?:b if a is not null, - * the value of the expression is a, if a is null then the value of the expression is - * b. + * Represents the Elvis operator ?:. For an expression a?:b if a is neither null + * nor an empty String, the value of the expression is a. + * If a is null or the empty String, then the value of the expression is b. * * @author Andy Clement * @author Juergen Hoeller @@ -43,8 +43,8 @@ public Elvis(int startPos, int endPos, SpelNodeImpl... args) { /** - * Evaluate the condition and if not null, return it. - * If it is null, return the other value. + * Evaluate the condition and if neither null nor an empty String, return it. + * If it is null or an empty String, return the other value. * @param state the expression state * @throws EvaluationException if the condition does not evaluate correctly * to a boolean or there is a problem executing the chosen alternative diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java index f2d7c0a5e953..c05cfad67435 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * Utility methods (formatters, etc) used during parsing and evaluation. @@ -51,10 +50,9 @@ static String formatMethodForMessage(String name, List argumentT *

      A String array will have the formatted name "java.lang.String[]". * @param clazz the Class whose name is to be formatted * @return a formatted String suitable for message inclusion - * @see ClassUtils#getQualifiedName(Class) */ static String formatClassNameForMessage(@Nullable Class clazz) { - return (clazz != null ? ClassUtils.getQualifiedName(clazz) : "null"); + return (clazz != null ? clazz.getTypeName() : "null"); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java index 4ad5b0dda907..8ffed18ca766 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.expression.spel.ast; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.StringJoiner; @@ -37,16 +39,21 @@ import org.springframework.util.ReflectionUtils; /** - * A function reference is of the form "#someFunction(a,b,c)". Functions may be defined - * in the context prior to the expression being evaluated. Functions may also be static - * Java methods, registered in the context prior to invocation of the expression. + * A function reference is of the form "#someFunction(a,b,c)". * - *

      Functions are very simplistic. The arguments are not part of the definition - * (right now), so the names must be unique. + *

      Functions can be either a {@link Method} (for static Java methods) or a + * {@link MethodHandle} and must be registered in the context prior to evaluation + * of the expression. See the {@code registerFunction()} methods in + * {@link org.springframework.expression.spel.support.StandardEvaluationContext} + * for details. * * @author Andy Clement * @author Juergen Hoeller + * @author Simon Baslé + * @author Sam Brannen * @since 3.0 + * @see org.springframework.expression.spel.support.StandardEvaluationContext#registerFunction(String, Method) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#registerFunction(String, MethodHandle) */ public class FunctionReference extends SpelNodeImpl { @@ -70,36 +77,51 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep if (value == TypedValue.NULL) { throw new SpelEvaluationException(getStartPosition(), SpelMessage.FUNCTION_NOT_DEFINED, this.name); } - if (!(value.getValue() instanceof Method function)) { - // Possibly a static Java method registered as a function - throw new SpelEvaluationException( - SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, this.name, value.getClass()); - } + Object function = value.getValue(); - try { - return executeFunctionJLRMethod(state, function); + // Static Java method registered via a Method. + // Note: "javaMethod" cannot be named "method" due to a bug in Checkstyle. + if (function instanceof Method javaMethod) { + try { + return executeFunctionViaMethod(state, javaMethod); + } + catch (SpelEvaluationException ex) { + ex.setPosition(getStartPosition()); + throw ex; + } } - catch (SpelEvaluationException ex) { - ex.setPosition(getStartPosition()); - throw ex; + + // Function registered via a MethodHandle. + if (function instanceof MethodHandle methodHandle) { + try { + return executeFunctionViaMethodHandle(state, methodHandle); + } + catch (SpelEvaluationException ex) { + ex.setPosition(getStartPosition()); + throw ex; + } } + + // Neither a Method nor a MethodHandle? + throw new SpelEvaluationException( + SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, this.name, value.getClass()); } /** - * Execute a function represented as a {@code java.lang.reflect.Method}. + * Execute a function represented as a {@link Method}. * @param state the expression evaluation state * @param method the method to invoke * @return the return value of the invoked Java method * @throws EvaluationException if there is any problem invoking the method */ - private TypedValue executeFunctionJLRMethod(ExpressionState state, Method method) throws EvaluationException { + private TypedValue executeFunctionViaMethod(ExpressionState state, Method method) throws EvaluationException { Object[] functionArgs = getArguments(state); if (!method.isVarArgs()) { int declaredParamCount = method.getParameterCount(); if (declaredParamCount != functionArgs.length) { throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, - functionArgs.length, declaredParamCount); + this.name, functionArgs.length, declaredParamCount); } } if (!Modifier.isStatic(method.getModifiers())) { @@ -138,6 +160,109 @@ private TypedValue executeFunctionJLRMethod(ExpressionState state, Method method } } + /** + * Execute a function represented as {@link MethodHandle}. + *

      Method types that take no arguments (fully bound handles or static methods + * with no parameters) can use {@link MethodHandle#invoke(Object...)} which is the most + * efficient. Otherwise, {@link MethodHandle#invokeWithArguments(Object...)} is used. + * @param state the expression evaluation state + * @param methodHandle the method handle to invoke + * @return the return value of the invoked Java method + * @throws EvaluationException if there is any problem invoking the method + * @since 6.1 + */ + private TypedValue executeFunctionViaMethodHandle(ExpressionState state, MethodHandle methodHandle) throws EvaluationException { + Object[] functionArgs = getArguments(state); + MethodType declaredParams = methodHandle.type(); + int spelParamCount = functionArgs.length; + int declaredParamCount = declaredParams.parameterCount(); + + // We don't use methodHandle.isVarargsCollector(), because a MethodHandle created via + // MethodHandle#bindTo() is "never a variable-arity method handle, even if the original + // target method handle was." Thus, we merely assume/suspect that varargs are supported + // if the last parameter type is an array. + boolean isSuspectedVarargs = declaredParams.lastParameterType().isArray(); + + if (isSuspectedVarargs) { + if (spelParamCount < declaredParamCount - 1) { + // Varargs, but the number of provided arguments (potentially 0) is insufficient + // for a varargs invocation for the number of declared parameters. + // + // As stated in the Javadoc for MethodHandle#asVarargsCollector(), "the caller + // must supply, at a minimum, N-1 arguments, where N is the arity of the target." + throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, + this.name, spelParamCount, (declaredParamCount - 1) + " or more"); + } + } + else if (spelParamCount != declaredParamCount) { + // Incorrect number and not varargs. Perhaps a subset of arguments was provided, + // but the MethodHandle wasn't bound? + throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, + this.name, spelParamCount, declaredParamCount); + } + + // simplest case: the MethodHandle is fully bound or represents a static method with no params: + if (declaredParamCount == 0) { + try { + return new TypedValue(methodHandle.invoke()); + } + catch (Throwable ex) { + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_FUNCTION_CALL, + this.name, ex.getMessage()); + } + finally { + // Note: we consider MethodHandles not compilable + this.exitTypeDescriptor = null; + this.method = null; + } + } + + // more complex case, we need to look at conversion and varargs repackaging + Integer varArgPosition = null; + if (isSuspectedVarargs) { + varArgPosition = declaredParamCount - 1; + } + TypeConverter converter = state.getEvaluationContext().getTypeConverter(); + ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition); + + if (isSuspectedVarargs) { + if (declaredParamCount == 1) { + // We only repackage the varargs if it is the ONLY argument -- for example, + // when we are dealing with a bound MethodHandle. + functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation( + methodHandle.type().parameterArray(), functionArgs); + } + else if (spelParamCount == declaredParamCount) { + // If the varargs were supplied already packaged in an array, we have to create + // a new array, add the non-varargs arguments to the beginning of that array, + // and add the unpackaged varargs arguments to the end of that array. The reason + // is that MethodHandle.invokeWithArguments(Object...) does not expect varargs + // to be packaged in an array, in contrast to how method invocation works with + // reflection. + int actualVarargsIndex = functionArgs.length - 1; + if (actualVarargsIndex >= 0 && functionArgs[actualVarargsIndex] instanceof Object[] argsToUnpack) { + Object[] newArgs = new Object[actualVarargsIndex + argsToUnpack.length]; + System.arraycopy(functionArgs, 0, newArgs, 0, actualVarargsIndex); + System.arraycopy(argsToUnpack, 0, newArgs, actualVarargsIndex, argsToUnpack.length); + functionArgs = newArgs; + } + } + } + + try { + return new TypedValue(methodHandle.invokeWithArguments(functionArgs)); + } + catch (Throwable ex) { + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_FUNCTION_CALL, + this.name, ex.getMessage()); + } + finally { + // Note: we consider MethodHandles not compilable + this.exitTypeDescriptor = null; + this.method = null; + } + } + @Override public String toStringAST() { StringJoiner sj = new StringJoiner(",", "(", ")"); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 7e80a576c13b..4b4b3160265c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.StringJoiner; import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; @@ -54,8 +53,6 @@ * @author Sam Brannen * @since 3.0 */ -// TODO support multidimensional arrays -// TODO support correct syntax for multidimensional [][][] and not [,,,] public class Indexer extends SpelNodeImpl { private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} @@ -180,12 +177,11 @@ else if (target instanceof Collection collection) { } else { this.indexedType = IndexedType.STRING; - return new StringIndexingLValue((String) target, idx, targetDescriptor); + return new StringIndexingValueRef((String) target, idx, targetDescriptor); } } // Try and treat the index value as a property of the context object - // TODO: could call the conversion service to convert the value to a String TypeDescriptor valueType = indexValue.getTypeDescriptor(); if (valueType != null && String.class == valueType.getType()) { this.indexedType = IndexedType.OBJECT; @@ -209,7 +205,7 @@ else if (this.indexedType == IndexedType.MAP) { return (this.children[0] instanceof PropertyOrFieldReference || this.children[0].isCompilable()); } else if (this.indexedType == IndexedType.OBJECT) { - // If the string name is changing the accessor is clearly going to change (so no compilation possible) + // If the string name is changing, the accessor is clearly going to change (so no compilation possible) return (this.cachedReadAccessor != null && this.cachedReadAccessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor && getChild(0) instanceof StringLiteral); @@ -225,54 +221,60 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { cf.loadTarget(mv); } + SpelNodeImpl index = this.children[0]; + if (this.indexedType == IndexedType.ARRAY) { - int insn; - if ("D".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[D"); - insn = DALOAD; - } - else if ("F".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[F"); - insn = FALOAD; - } - else if ("J".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[J"); - insn = LALOAD; - } - else if ("I".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[I"); - insn = IALOAD; - } - else if ("S".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[S"); - insn = SALOAD; - } - else if ("B".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[B"); - insn = BALOAD; - } - else if ("C".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[C"); - insn = CALOAD; - } - else { - mv.visitTypeInsn(CHECKCAST, "["+ this.exitTypeDescriptor + - (CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";")); - //depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;"); - insn = AALOAD; - } - SpelNodeImpl index = this.children[0]; - cf.enterCompilationScope(); - index.generateCode(mv, cf); - cf.exitCompilationScope(); + String exitTypeDescriptor = this.exitTypeDescriptor; + Assert.state(exitTypeDescriptor != null, "Array not compilable without descriptor"); + int insn = switch (exitTypeDescriptor) { + case "D" -> { + mv.visitTypeInsn(CHECKCAST, "[D"); + yield DALOAD; + } + case "F" -> { + mv.visitTypeInsn(CHECKCAST, "[F"); + yield FALOAD; + } + case "J" -> { + mv.visitTypeInsn(CHECKCAST, "[J"); + yield LALOAD; + } + case "I" -> { + mv.visitTypeInsn(CHECKCAST, "[I"); + yield IALOAD; + } + case "S" -> { + mv.visitTypeInsn(CHECKCAST, "[S"); + yield SALOAD; + } + case "B" -> { + mv.visitTypeInsn(CHECKCAST, "[B"); + // byte and boolean arrays are both loaded via BALOAD + yield BALOAD; + } + case "Z" -> { + mv.visitTypeInsn(CHECKCAST, "[Z"); + // byte and boolean arrays are both loaded via BALOAD + yield BALOAD; + } + case "C" -> { + mv.visitTypeInsn(CHECKCAST, "[C"); + yield CALOAD; + } + default -> { + mv.visitTypeInsn(CHECKCAST, "["+ exitTypeDescriptor + + (CodeFlow.isPrimitiveArray(exitTypeDescriptor) ? "" : ";")); + yield AALOAD; + } + }; + + generateIndexCode(mv, cf, index, int.class); mv.visitInsn(insn); } else if (this.indexedType == IndexedType.LIST) { mv.visitTypeInsn(CHECKCAST, "java/util/List"); - cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); - cf.exitCompilationScope(); + generateIndexCode(mv, cf, index, int.class); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true); } @@ -280,14 +282,12 @@ else if (this.indexedType == IndexedType.MAP) { mv.visitTypeInsn(CHECKCAST, "java/util/Map"); // Special case when the key is an unquoted string literal that will be parsed as // a property/field reference - if ((this.children[0] instanceof PropertyOrFieldReference reference)) { + if ((index instanceof PropertyOrFieldReference reference)) { String mapKeyName = reference.getName(); mv.visitLdcInsn(mapKeyName); } else { - cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); - cf.exitCompilationScope(); + generateIndexCode(mv, cf, index, Object.class); } mv.visitMethodInsn( INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true); @@ -323,58 +323,59 @@ else if (this.indexedType == IndexedType.OBJECT) { cf.pushDescriptor(this.exitTypeDescriptor); } + private void generateIndexCode(MethodVisitor mv, CodeFlow cf, SpelNodeImpl indexNode, Class indexType) { + String indexDesc = CodeFlow.toDescriptor(indexType); + generateCodeForArgument(mv, cf, indexNode, indexDesc); + } + @Override public String toStringAST() { - StringJoiner sj = new StringJoiner(",", "[", "]"); - for (int i = 0; i < getChildCount(); i++) { - sj.add(getChild(i).toStringAST()); - } - return sj.toString(); + return "[" + getChild(0).toStringAST() + "]"; } private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue, Class arrayComponentType) throws EvaluationException { - if (arrayComponentType == Boolean.TYPE) { + if (arrayComponentType == boolean.class) { boolean[] array = (boolean[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Boolean.class); + array[idx] = convertValue(converter, newValue, boolean.class); } - else if (arrayComponentType == Byte.TYPE) { + else if (arrayComponentType == byte.class) { byte[] array = (byte[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Byte.class); + array[idx] = convertValue(converter, newValue, byte.class); } - else if (arrayComponentType == Character.TYPE) { + else if (arrayComponentType == char.class) { char[] array = (char[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Character.class); + array[idx] = convertValue(converter, newValue, char.class); } - else if (arrayComponentType == Double.TYPE) { + else if (arrayComponentType == double.class) { double[] array = (double[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Double.class); + array[idx] = convertValue(converter, newValue, double.class); } - else if (arrayComponentType == Float.TYPE) { + else if (arrayComponentType == float.class) { float[] array = (float[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Float.class); + array[idx] = convertValue(converter, newValue, float.class); } - else if (arrayComponentType == Integer.TYPE) { + else if (arrayComponentType == int.class) { int[] array = (int[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Integer.class); + array[idx] = convertValue(converter, newValue, int.class); } - else if (arrayComponentType == Long.TYPE) { + else if (arrayComponentType == long.class) { long[] array = (long[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Long.class); + array[idx] = convertValue(converter, newValue, long.class); } - else if (arrayComponentType == Short.TYPE) { + else if (arrayComponentType == short.class) { short[] array = (short[]) ctx; checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, Short.class); + array[idx] = convertValue(converter, newValue, short.class); } else { Object[] array = (Object[]) ctx; @@ -384,50 +385,50 @@ else if (arrayComponentType == Short.TYPE) { } private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationException { - Class arrayComponentType = ctx.getClass().getComponentType(); - if (arrayComponentType == Boolean.TYPE) { + Class arrayComponentType = ctx.getClass().componentType(); + if (arrayComponentType == boolean.class) { boolean[] array = (boolean[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "Z"; return array[idx]; } - else if (arrayComponentType == Byte.TYPE) { + else if (arrayComponentType == byte.class) { byte[] array = (byte[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "B"; return array[idx]; } - else if (arrayComponentType == Character.TYPE) { + else if (arrayComponentType == char.class) { char[] array = (char[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "C"; return array[idx]; } - else if (arrayComponentType == Double.TYPE) { + else if (arrayComponentType == double.class) { double[] array = (double[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "D"; return array[idx]; } - else if (arrayComponentType == Float.TYPE) { + else if (arrayComponentType == float.class) { float[] array = (float[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "F"; return array[idx]; } - else if (arrayComponentType == Integer.TYPE) { + else if (arrayComponentType == int.class) { int[] array = (int[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "I"; return array[idx]; } - else if (arrayComponentType == Long.TYPE) { + else if (arrayComponentType == long.class) { long[] array = (long[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "J"; return array[idx]; } - else if (arrayComponentType == Short.TYPE) { + else if (arrayComponentType == short.class) { short[] array = (short[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "S"; @@ -629,6 +630,8 @@ public void setValue(@Nullable Object newValue) { throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, this.name, ex.getMessage()); } + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString()); } @Override @@ -744,7 +747,7 @@ public boolean isWritable() { } - private class StringIndexingLValue implements ValueRef { + private class StringIndexingValueRef implements ValueRef { private final String target; @@ -752,7 +755,7 @@ private class StringIndexingLValue implements ValueRef { private final TypeDescriptor typeDescriptor; - public StringIndexingLValue(String target, int index, TypeDescriptor typeDescriptor) { + public StringIndexingValueRef(String target, int index, TypeDescriptor typeDescriptor) { this.target = target; this.index = index; this.typeDescriptor = typeDescriptor; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java index 8abf63ec6ddf..339326e97722 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -35,55 +36,60 @@ * * @author Andy Clement * @author Sam Brannen + * @author Harry Yang + * @author Semyon Danilov * @since 3.0.4 */ public class InlineList extends SpelNodeImpl { - // If the list is purely literals, it is a constant value and can be computed and cached @Nullable - private TypedValue constant; // TODO must be immutable list + private final TypedValue constant; public InlineList(int startPos, int endPos, SpelNodeImpl... args) { super(startPos, endPos, args); - checkIfConstant(); + this.constant = computeConstantValue(); } /** - * If all the components of the list are constants, or lists that themselves contain constants, then a constant list - * can be built to represent this node. This will speed up later getValue calls and reduce the amount of garbage + * If all the components of the list are constants, or lists that themselves + * contain constants, then a constant list can be built to represent this node. + *

      This will speed up later getValue calls and reduce the amount of garbage * created. */ - private void checkIfConstant() { - boolean isConstant = true; + @Nullable + private TypedValue computeConstantValue() { for (int c = 0, max = getChildCount(); c < max; c++) { SpelNode child = getChild(c); if (!(child instanceof Literal)) { if (child instanceof InlineList inlineList) { if (!inlineList.isConstant()) { - isConstant = false; + return null; } } - else { - isConstant = false; + else if (!(child instanceof OpMinus opMinus) || !opMinus.isNegativeNumberLiteral()) { + return null; } } } - if (isConstant) { - List constantList = new ArrayList<>(); - int childcount = getChildCount(); - for (int c = 0; c < childcount; c++) { - SpelNode child = getChild(c); - if (child instanceof Literal literal) { - constantList.add(literal.getLiteralValue().getValue()); - } - else if (child instanceof InlineList inlineList) { - constantList.add(inlineList.getConstantValue()); - } + + List constantList = new ArrayList<>(); + int childcount = getChildCount(); + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + for (int c = 0; c < childcount; c++) { + SpelNode child = getChild(c); + if (child instanceof Literal literal) { + constantList.add(literal.getLiteralValue().getValue()); + } + else if (child instanceof InlineList inlineList) { + constantList.add(inlineList.getConstantValue()); + } + else if (child instanceof OpMinus) { + constantList.add(child.getValue(expressionState)); } - this.constant = new TypedValue(Collections.unmodifiableList(constantList)); } + return new TypedValue(Collections.unmodifiableList(constantList)); } @Override @@ -105,8 +111,7 @@ public TypedValue getValueInternal(ExpressionState expressionState) throws Evalu public String toStringAST() { StringJoiner sj = new StringJoiner(",", "{", "}"); // String ast matches input string, not the 'toString()' of the resultant collection, which would use [] - int count = getChildCount(); - for (int c = 0; c < count; c++) { + for (int c = 0; c < getChildCount(); c++) { sj.add(getChild(c).toStringAST()); } return sj.toString(); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java index 7f63c356885b..a08c50217ef4 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -32,79 +33,87 @@ * * @author Andy Clement * @author Sam Brannen + * @author Harry Yang + * @author Semyon Danilov * @since 4.1 */ public class InlineMap extends SpelNodeImpl { - // If the map is purely literals, it is a constant value and can be computed and cached @Nullable - private TypedValue constant; + private final TypedValue constant; public InlineMap(int startPos, int endPos, SpelNodeImpl... args) { super(startPos, endPos, args); - checkIfConstant(); + this.constant = computeConstantValue(); } /** * If all the components of the map are constants, or lists/maps that themselves - * contain constants, then a constant list can be built to represent this node. - * This will speed up later getValue calls and reduce the amount of garbage created. + * contain constants, then a constant map can be built to represent this node. + *

      This will speed up later getValue calls and reduce the amount of garbage + * created. */ - private void checkIfConstant() { - boolean isConstant = true; + @Nullable + private TypedValue computeConstantValue() { for (int c = 0, max = getChildCount(); c < max; c++) { SpelNode child = getChild(c); if (!(child instanceof Literal)) { if (child instanceof InlineList inlineList) { if (!inlineList.isConstant()) { - isConstant = false; - break; + return null; } } else if (child instanceof InlineMap inlineMap) { if (!inlineMap.isConstant()) { - isConstant = false; - break; + return null; } } else if (!(c % 2 == 0 && child instanceof PropertyOrFieldReference)) { - isConstant = false; - break; + if (!(child instanceof OpMinus opMinus) || !opMinus.isNegativeNumberLiteral()) { + return null; + } } } } - if (isConstant) { - Map constantMap = new LinkedHashMap<>(); - int childCount = getChildCount(); - for (int c = 0; c < childCount; c++) { - SpelNode keyChild = getChild(c++); - SpelNode valueChild = getChild(c); - Object key = null; - Object value = null; - if (keyChild instanceof Literal literal) { - key = literal.getLiteralValue().getValue(); - } - else if (keyChild instanceof PropertyOrFieldReference propertyOrFieldReference) { - key = propertyOrFieldReference.getName(); - } - else { - return; - } - if (valueChild instanceof Literal literal) { - value = literal.getLiteralValue().getValue(); - } - else if (valueChild instanceof InlineList inlineList) { - value = inlineList.getConstantValue(); - } - else if (valueChild instanceof InlineMap inlineMap) { - value = inlineMap.getConstantValue(); - } - constantMap.put(key, value); + + Map constantMap = new LinkedHashMap<>(); + int childCount = getChildCount(); + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + for (int c = 0; c < childCount; c++) { + SpelNode keyChild = getChild(c++); + Object key; + if (keyChild instanceof Literal literal) { + key = literal.getLiteralValue().getValue(); + } + else if (keyChild instanceof PropertyOrFieldReference propertyOrFieldReference) { + key = propertyOrFieldReference.getName(); + } + else if (keyChild instanceof OpMinus) { + key = keyChild.getValue(expressionState); + } + else { + return null; + } + + SpelNode valueChild = getChild(c); + Object value = null; + if (valueChild instanceof Literal literal) { + value = literal.getLiteralValue().getValue(); + } + else if (valueChild instanceof InlineList inlineList) { + value = inlineList.getConstantValue(); + } + else if (valueChild instanceof InlineMap inlineMap) { + value = inlineMap.getConstantValue(); + } + else if (valueChild instanceof OpMinus) { + value = valueChild.getValue(expressionState); } - this.constant = new TypedValue(Collections.unmodifiableMap(constantMap)); + constantMap.put(key, value); } + return new TypedValue(Collections.unmodifiableMap(constantMap)); } @Override @@ -116,7 +125,6 @@ public TypedValue getValueInternal(ExpressionState expressionState) throws Evalu Map returnValue = new LinkedHashMap<>(); int childcount = getChildCount(); for (int c = 0; c < childcount; c++) { - // TODO allow for key being PropertyOrFieldReference like Indexer on maps SpelNode keyChild = getChild(c++); Object key = null; if (keyChild instanceof PropertyOrFieldReference reference) { @@ -126,7 +134,7 @@ public TypedValue getValueInternal(ExpressionState expressionState) throws Evalu key = keyChild.getValue(expressionState); } Object value = getChild(c).getValue(expressionState); - returnValue.put(key, value); + returnValue.put(key, value); } return new TypedValue(returnValue); } @@ -135,8 +143,7 @@ public TypedValue getValueInternal(ExpressionState expressionState) throws Evalu @Override public String toStringAST() { StringBuilder sb = new StringBuilder("{"); - int count = getChildCount(); - for (int c = 0; c < count; c++) { + for (int c = 0; c < getChildCount(); c++) { if (c > 0) { sb.append(','); } @@ -149,7 +156,7 @@ public String toStringAST() { } /** - * Return whether this list is a constant value. + * Return whether this map is a constant value. */ public boolean isConstant() { return this.constant != null; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java index 6455f3a9d9bc..dbd9e0f2836b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Semyon Danilov */ public abstract class Literal extends SpelNodeImpl { @@ -52,6 +53,18 @@ public final TypedValue getValueInternal(ExpressionState state) throws SpelEvalu return getLiteralValue(); } + /** + * Determine if this literal represents a number. + * @return {@code true} if this literal represents a number + * @since 6.1 + */ + public boolean isNumberLiteral() { + return (this instanceof IntLiteral || + this instanceof LongLiteral || + this instanceof FloatLiteral || + this instanceof RealLiteral); + } + @Override public String toString() { return String.valueOf(getLiteralValue().getValue()); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 0e7af3e02eb8..ed0cdca8846f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,6 +77,7 @@ public MethodReference(boolean nullSafe, String methodName, int startPos, int en * Does this node represent a null-safe method reference? * @since 6.0.13 */ + @Override public final boolean isNullSafe() { return this.nullSafe; } @@ -314,20 +315,22 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { boolean isStaticMethod = Modifier.isStatic(method.getModifiers()); String descriptor = cf.lastDescriptor(); - Label skipIfNull = null; if (descriptor == null && !isStaticMethod) { // Nothing on the stack but something is needed cf.loadTarget(mv); } - if ((descriptor != null || !isStaticMethod) && this.nullSafe) { - mv.visitInsn(DUP); + + Label skipIfNull = null; + if (this.nullSafe && (descriptor != null || !isStaticMethod)) { skipIfNull = new Label(); Label continueLabel = new Label(); + mv.visitInsn(DUP); mv.visitJumpInsn(IFNONNULL, continueLabel); CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor); mv.visitJumpInsn(GOTO, skipIfNull); mv.visitLabel(continueLabel); } + if (descriptor != null && isStaticMethod) { // Something on the stack when nothing is needed mv.visitInsn(POP); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java index d61e8d641062..0d7c12042546 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,11 +39,13 @@ */ public class OpDec extends Operator { + private static final String DEC = "--"; + private final boolean postfix; // false means prefix public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands) { - super("--", startPos, endPos, operands); + super(DEC, startPos, endPos, operands); this.postfix = postfix; Assert.notEmpty(operands, "Operands must not be empty"); } @@ -51,6 +53,10 @@ public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST()); + } + SpelNodeImpl operand = getLeftOperand(); // The operand is going to be read and then assigned to, we don't want to evaluate it twice. @@ -133,7 +139,8 @@ else if (op1 instanceof Byte) { @Override public String toStringAST() { - return getLeftOperand().toStringAST() + "--"; + String ast = getLeftOperand().toStringAST(); + return (this.postfix ? ast + DEC : DEC + ast); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java index f6dc184c0f5e..077ef942c4c7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,11 +39,13 @@ */ public class OpInc extends Operator { + private static final String INC = "++"; + private final boolean postfix; // false means prefix public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands) { - super("++", startPos, endPos, operands); + super(INC, startPos, endPos, operands); this.postfix = postfix; Assert.notEmpty(operands, "Operands must not be empty"); } @@ -51,6 +53,10 @@ public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST()); + } + SpelNodeImpl operand = getLeftOperand(); ValueRef valueRef = operand.getValueRef(state); @@ -104,7 +110,7 @@ else if (op1 instanceof Byte) { } } - // set the name value + // set the new value try { valueRef.setValue(newValue.getValue()); } @@ -128,7 +134,8 @@ else if (op1 instanceof Byte) { @Override public String toStringAST() { - return getLeftOperand().toStringAST() + "++"; + String ast = getLeftOperand().toStringAST(); + return (this.postfix ? ast + INC : INC + ast); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java index 1098e15897c7..01c1b95deda1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ *

        *
      • subtraction of numbers *
      • subtraction of an int from a string of one character - * (effectively decreasing that character), so 'd'-3='a' + * (effectively decreasing that character), so {@code 'd' - 3 = 'a'} *
      * *

      It can be used as a unary operator for numbers. @@ -44,6 +44,7 @@ * @author Juergen Hoeller * @author Giovanni Dall'Oglio Risso * @author Sam Brannen + * @author Semyon Danilov * @since 3.0 */ public class OpMinus extends Operator { @@ -53,6 +54,17 @@ public OpMinus(int startPos, int endPos, SpelNodeImpl... operands) { } + /** + * Determine if this operator is a unary minus and its child is a + * {@linkplain Literal#isNumberLiteral() number literal}. + * @return {@code true} if it is a negative number literal + * @since 6.1 + */ + public boolean isNegativeNumberLiteral() { + return (this.children.length == 1 && this.children[0] instanceof Literal literal && + literal.isNumberLiteral()); + } + @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { SpelNodeImpl leftOp = getLeftOperand(); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java index 12733435a90d..73da3bb850ea 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,12 +26,18 @@ import org.springframework.expression.spel.support.BooleanTypedValue; /** - * Represents the between operator. The left operand to between must be a single value and - * the right operand must be a list - this operator returns true if the left operand is - * between (using the registered comparator) the two elements in the list. The definition - * of between being inclusive follows the SQL BETWEEN definition. + * Represents the {@code between} operator. + * + *

      The left operand must be a single value, and the right operand must be a + * 2-element list which defines a range from a lower bound to an upper bound. + * + *

      This operator returns {@code true} if the left operand is greater than or + * equal to the lower bound and less than or equal to the upper bound. Consequently, + * {@code 1 between {1, 5}} evaluates to {@code true}, while {@code 1 between {5, 1}} + * evaluates to {@code false}. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class OperatorBetween extends Operator { @@ -44,7 +50,7 @@ public OperatorBetween(int startPos, int endPos, SpelNodeImpl... operands) { /** * Returns a boolean based on whether a value is in the range expressed. The first * operand is any value whilst the second is a list of two values - those two values - * being the bounds allowed for the first operand (inclusive). + * being the lower and upper bounds allowed for the first operand (inclusive). * @param state the expression state * @return true if the left operand is in the range specified, false otherwise * @throws EvaluationException if there is a problem evaluating the expression diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index b9a9c2cc0b9d..121b7245ae92 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,15 @@ public Projection(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expre } + /** + * Does this node represent a null-safe projection operation? + * @since 6.1.6 + */ + @Override + public final boolean isNullSafe() { + return this.nullSafe; + } + @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { return getValueRef(state).getValue(); @@ -60,10 +69,7 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep @Override protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { TypedValue op = state.getActiveContextObject(); - Object operand = op.getValue(); - boolean operandIsArray = ObjectUtils.isArray(operand); - // TypeDescriptor operandTypeDescriptor = op.getTypeDescriptor(); // When the input is a map, we push a special context object on the stack // before calling the specified operation. This special context object @@ -86,6 +92,7 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); // TODO unable to build correct type descriptor } + boolean operandIsArray = ObjectUtils.isArray(operand); if (operand instanceof Iterable || operandIsArray) { Iterable data = (operand instanceof Iterable iterable ? iterable : Arrays.asList(ObjectUtils.toObjectArray(operand))); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index dc891da4a4f6..bb2e17197376 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,7 @@ public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, in /** * Does this node represent a null-safe property or field reference? */ + @Override public boolean isNullSafe() { return this.nullSafe; } @@ -90,7 +91,7 @@ public String getName() { @Override public ValueRef getValueRef(ExpressionState state) throws EvaluationException { - return new AccessorLValue(this, state.getActiveContextObject(), state.getEvaluationContext(), + return new AccessorValueRef(this, state.getActiveContextObject(), state.getEvaluationContext(), state.getConfiguration().isAutoGrowNullReferences()); } @@ -181,7 +182,7 @@ private TypedValue readProperty(TypedValue contextObject, EvaluationContext eval throws EvaluationException { Object targetObject = contextObject.getValue(); - if (targetObject == null && this.nullSafe) { + if (targetObject == null && isNullSafe()) { return TypedValue.NULL; } @@ -233,7 +234,7 @@ private void writeProperty( TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue) throws EvaluationException { - if (contextObject.getValue() == null && this.nullSafe) { + if (contextObject.getValue() == null && isNullSafe()) { return; } if (contextObject.getValue() == null) { @@ -353,7 +354,7 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { } Label skipIfNull = null; - if (this.nullSafe) { + if (isNullSafe()) { mv.visitInsn(DUP); skipIfNull = new Label(); Label continueLabel = new Label(); @@ -381,7 +382,7 @@ void setExitTypeDescriptor(String descriptor) { // If this property or field access would return a primitive - and yet // it is also marked null safe - then the exit type descriptor must be // promoted to the box type to allow a null value to be passed on - if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) { + if (isNullSafe() && CodeFlow.isPrimitive(descriptor)) { this.originalPrimitiveExitTypeDescriptor = descriptor; this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); } @@ -391,7 +392,7 @@ void setExitTypeDescriptor(String descriptor) { } - private static class AccessorLValue implements ValueRef { + private static class AccessorValueRef implements ValueRef { private final PropertyOrFieldReference ref; @@ -401,7 +402,7 @@ private static class AccessorLValue implements ValueRef { private final boolean autoGrowNullReferences; - public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject, + public AccessorValueRef(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject, EvaluationContext evalContext, boolean autoGrowNullReferences) { this.ref = propertyOrFieldReference; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index 72f9aa6f0e88..2593998d84d0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,9 @@ /** * Represents selection over a map or collection. - * For example: {1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'} returns [2, 4, 6, 8, 10] + * + *

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

      Basically a subset of the input data is returned based on the * evaluation of the expression supplied as selection criteria. @@ -76,6 +78,15 @@ public Selection(boolean nullSafe, int variant, int startPos, int endPos, SpelNo } + /** + * Does this node represent a null-safe selection operation? + * @since 6.1.6 + */ + @Override + public final boolean isNullSafe() { + return this.nullSafe; + } + @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { return getValueRef(state).getValue(); @@ -100,11 +111,10 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException Object val = selectionCriteria.getValueInternal(state).getValue(); if (val instanceof Boolean b) { if (b) { + result.put(entry.getKey(), entry.getValue()); if (this.variant == FIRST) { - result.put(entry.getKey(), entry.getValue()); return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); } - result.put(entry.getKey(), entry.getValue()); lastKey = entry.getKey(); } } @@ -120,22 +130,22 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException } if ((this.variant == FIRST || this.variant == LAST) && result.isEmpty()) { - return new ValueRef.TypedValueHolderValueRef(new TypedValue(null), this); + return new ValueRef.TypedValueHolderValueRef(TypedValue.NULL, this); } if (this.variant == LAST) { Map resultMap = new HashMap<>(); Object lastValue = result.get(lastKey); - resultMap.put(lastKey,lastValue); - return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultMap),this); + resultMap.put(lastKey, lastValue); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultMap), this); } - return new ValueRef.TypedValueHolderValueRef(new TypedValue(result),this); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); } if (operand instanceof Iterable || ObjectUtils.isArray(operand)) { - Iterable data = (operand instanceof Iterable iterable ? - iterable : Arrays.asList(ObjectUtils.toObjectArray(operand))); + Iterable data = (operand instanceof Iterable iterable ? iterable : + Arrays.asList(ObjectUtils.toObjectArray(operand))); List result = new ArrayList<>(); int index = 0; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java index cd528937cc6d..3fb9208bc1b8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes { protected volatile String exitTypeDescriptor; - public SpelNodeImpl(int startPos, int endPos, SpelNodeImpl... operands) { + public SpelNodeImpl(int startPos, int endPos, @Nullable SpelNodeImpl... operands) { this.startPos = startPos; this.endPos = endPos; if (!ObjectUtils.isEmpty(operands)) { @@ -149,7 +149,7 @@ public void setValue(ExpressionState expressionState, @Nullable Object newValue) public TypedValue setValueInternal(ExpressionState expressionState, Supplier valueSupplier) throws EvaluationException { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass()); + throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass().getName()); } @Override @@ -181,6 +181,16 @@ public int getEndPosition() { return this.endPos; } + /** + * Determine if this node is the target of a null-safe navigation operation. + *

      The default implementation returns {@code false}. + * @return {@code true} if this node is the target of a null-safe operation + * @since 6.1.6 + */ + public boolean isNullSafe() { + return false; + } + /** * Check whether a node can be compiled to bytecode. The reasoning in each node may * be different but will typically involve checking whether the exit type descriptor diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java index 0347ecddd326..9702d8a87384 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,42 +33,42 @@ public enum TypeCode { /** * A {@code boolean}. */ - BOOLEAN(Boolean.TYPE), + BOOLEAN(boolean.class), /** * A {@code char}. */ - CHAR(Character.TYPE), + CHAR(char.class), /** * A {@code byte}. */ - BYTE(Byte.TYPE), + BYTE(byte.class), /** * A {@code short}. */ - SHORT(Short.TYPE), + SHORT(short.class), /** * An {@code int}. */ - INT(Integer.TYPE), + INT(int.class), /** * A {@code long}. */ - LONG(Long.TYPE), + LONG(long.class), /** * A {@code float}. */ - FLOAT(Float.TYPE), + FLOAT(float.class), /** * A {@code double}. */ - DOUBLE(Double.TYPE); + DOUBLE(double.class); private final Class type; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java index 5f09958fbe65..3effcf1858e0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,28 +102,28 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { // TODO Future optimization - if followed by a static method call, skip generating code here Assert.state(this.type != null, "No type available"); if (this.type.isPrimitive()) { - if (this.type == Boolean.TYPE) { + if (this.type == boolean.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;"); } - else if (this.type == Byte.TYPE) { + else if (this.type == byte.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;"); } - else if (this.type == Character.TYPE) { + else if (this.type == char.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;"); } - else if (this.type == Double.TYPE) { + else if (this.type == double.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;"); } - else if (this.type == Float.TYPE) { + else if (this.type == float.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;"); } - else if (this.type == Integer.TYPE) { + else if (this.type == int.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;"); } - else if (this.type == Long.TYPE) { + else if (this.type == long.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;"); } - else if (this.type == Short.TYPE) { + else if (this.type == short.class) { mv.visitFieldInsn(GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;"); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java index 97dae78e902e..31cf1f699d0d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,8 @@ import org.springframework.lang.Nullable; /** - * Represents a variable reference — for example, {@code #someVar}. Note - * that this is different than a local variable like {@code $someVar}. + * Represents a variable reference — for example, {@code #root}, {@code #this}, + * {@code #someVar}, etc. * * @author Andy Clement * @author Sam Brannen @@ -38,10 +38,11 @@ */ public class VariableReference extends SpelNodeImpl { - // Well known variables: - private static final String THIS = "this"; // currently active context object + /** Currently active context object. */ + private static final String THIS = "this"; - private static final String ROOT = "root"; // root context object + /** Root context object. */ + private static final String ROOT = "root"; private final String name; @@ -55,41 +56,63 @@ public VariableReference(String variableName, int startPos, int endPos) { @Override public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException { - if (this.name.equals(THIS)) { + if (THIS.equals(this.name)) { return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(), this); } - if (this.name.equals(ROOT)) { + if (ROOT.equals(this.name)) { return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(), this); } TypedValue result = state.lookupVariable(this.name); - // a null value will mean either the value was null or the variable was not found + // A null value in the returned VariableRef will mean either the value was + // null or the variable was not found. return new VariableRef(this.name, result, state.getEvaluationContext()); } @Override public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException { - if (this.name.equals(THIS)) { - return state.getActiveContextObject(); + TypedValue result; + if (THIS.equals(this.name)) { + result = state.getActiveContextObject(); + // If the active context object (#this) is not the root context object (#root), + // that means that #this is being evaluated within a nested scope (for example, + // collection selection or collection project), which is not a compilable + // expression, so we return the result without setting the exit type descriptor. + if (result != state.getRootContextObject()) { + return result; + } } - if (this.name.equals(ROOT)) { - TypedValue result = state.getRootContextObject(); - this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(result.getValue()); - return result; + else if (ROOT.equals(this.name)) { + result = state.getRootContextObject(); } - TypedValue result = state.lookupVariable(this.name); - Object value = result.getValue(); + else { + result = state.lookupVariable(this.name); + } + setExitTypeDescriptor(result.getValue()); + + // A null value in the returned TypedValue will mean either the value was + // null or the variable was not found. + return result; + } + + /** + * Set the exit type descriptor for the supplied value. + *

      If the value is {@code null}, we set the exit type descriptor to + * {@link Object}. + *

      If the value's type is not public, {@link #generateCode} would insert + * a checkcast to the non-public type in the generated byte code which would + * result in an {@link IllegalAccessError} when the compiled byte code is + * invoked. Thus, as a preventative measure, we set the exit type descriptor + * to {@code Object} in such cases. If resorting to {@code Object} is not + * sufficient, we could consider traversing the hierarchy to find the first + * public type. + */ + private void setExitTypeDescriptor(@Nullable Object value) { if (value == null || !Modifier.isPublic(value.getClass().getModifiers())) { - // If the type is not public then when generateCode produces a checkcast to it - // then an IllegalAccessError will occur. - // If resorting to Object isn't sufficient, the hierarchy could be traversed for - // the first public type. this.exitTypeDescriptor = "Ljava/lang/Object"; } else { this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(value); } - // a null value will mean either the value was null or the variable was not found - return result; } @Override @@ -106,7 +129,7 @@ public String toStringAST() { @Override public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException { - return !(this.name.equals(THIS) || this.name.equals(ROOT)); + return !(THIS.equals(this.name) || ROOT.equals(this.name)); } @Override @@ -116,13 +139,14 @@ public boolean isCompilable() { @Override public void generateCode(MethodVisitor mv, CodeFlow cf) { - if (this.name.equals(ROOT)) { - mv.visitVarInsn(ALOAD,1); + if (THIS.equals(this.name) || ROOT.equals(this.name)) { + mv.visitVarInsn(ALOAD, 1); } else { mv.visitVarInsn(ALOAD, 2); mv.visitLdcInsn(this.name); - mv.visitMethodInsn(INVOKEINTERFACE, "org/springframework/expression/EvaluationContext", "lookupVariable", "(Ljava/lang/String;)Ljava/lang/Object;",true); + mv.visitMethodInsn(INVOKEINTERFACE, "org/springframework/expression/EvaluationContext", + "lookupVariable", "(Ljava/lang/String;)Ljava/lang/Object;", true); } CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor); cf.pushDescriptor(this.exitTypeDescriptor); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index cf135d08ed23..8d2c0d2a3ede 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -966,7 +966,7 @@ private boolean peekToken(TokenKind desiredTokenKind, boolean consumeIfMatched) if (desiredTokenKind == TokenKind.IDENTIFIER) { // Might be one of the textual forms of the operators (e.g. NE for != ) - // in which case we can treat it as an identifier. The list is represented here: - // Tokenizer.alternativeOperatorNames and those ones are in order in the TokenKind enum. + // Tokenizer.ALTERNATIVE_OPERATOR_NAMES and those ones are in order in the TokenKind enum. if (t.kind.ordinal() >= TokenKind.DIV.ordinal() && t.kind.ordinal() <= TokenKind.NOT.ordinal() && t.data != null) { // if t.data were null, we'd know it wasn't the textual form, it was the symbol form diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java index 2f73dbf321c9..1ca414375b5b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java @@ -234,11 +234,7 @@ public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) { if (compiler == null) { // Full lock now since we're creating a child ClassLoader synchronized (compilers) { - compiler = compilers.get(clToUse); - if (compiler == null) { - compiler = new SpelCompiler(clToUse); - compilers.put(clToUse, compiler); - } + return compilers.computeIfAbsent(clToUse, SpelCompiler::new); } } return compiler; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java index f00f26a30037..e7fcde56b692 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,41 +19,49 @@ import org.springframework.lang.Nullable; /** - * Holder for a kind of token, the associated data and its position in the input data - * stream (start/end). + * Holder for a kind of token, the associated data, and its position in the input + * data stream (start/end). * * @author Andy Clement * @since 3.0 */ class Token { - TokenKind kind; + final TokenKind kind; @Nullable - String data; + final String data; - int startPos; // index of first character + final int startPos; - int endPos; // index of char after the last character + final int endPos; /** * Constructor for use when there is no particular data for the token - * (e.g. TRUE or '+') - * @param startPos the exact start - * @param endPos the index to the last character + * (e.g. TRUE or '+'). + * @param tokenKind the kind of token + * @param startPos the exact start position + * @param endPos the index of the last character */ Token(TokenKind tokenKind, int startPos, int endPos) { + this(tokenKind, null, startPos, endPos); + } + + /** + * Constructor for use when there is data for the token. + * @param tokenKind the kind of token + * @param tokenData the data for the token + * @param startPos the exact start position + * @param endPos the index of the last character + */ + Token(TokenKind tokenKind, @Nullable char[] tokenData, int startPos, int endPos) { this.kind = tokenKind; + this.data = (tokenData != null ? new String(tokenData) : null); this.startPos = startPos; this.endPos = endPos; } - Token(TokenKind tokenKind, char[] tokenData, int startPos, int endPos) { - this(tokenKind, startPos, endPos); - this.data = new String(tokenData); - } - public TokenKind getKind() { return this.kind; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java index 461aaf951980..8d80fc8a3364 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,19 @@ * @author Andy Clement * @author Juergen Hoeller * @author Phillip Webb + * @author Sam Brannen * @since 3.0 */ class Tokenizer { - // If this gets changed, it must remain sorted... + /** + * Alternative textual operator names which must match enum constant names + * in {@link TokenKind}. + *

      Note that {@code AND} and {@code OR} are also alternative textual + * names, but they are handled later in {@link InternalSpelExpressionParser}. + *

      If this list gets changed, it must remain sorted since we use it with + * {@link Arrays#binarySearch(Object[], Object)}. + */ private static final String[] ALTERNATIVE_OPERATOR_NAMES = {"DIV", "EQ", "GE", "GT", "LE", "LT", "MOD", "NE", "NOT"}; @@ -44,8 +52,6 @@ class Tokenizer { private static final byte IS_HEXDIGIT = 0x02; - private static final byte IS_ALPHA = 0x04; - static { for (int ch = '0'; ch <= '9'; ch++) { FLAGS[ch] |= IS_DIGIT | IS_HEXDIGIT; @@ -56,12 +62,6 @@ class Tokenizer { for (int ch = 'a'; ch <= 'f'; ch++) { FLAGS[ch] |= IS_HEXDIGIT; } - for (int ch = 'A'; ch <= 'Z'; ch++) { - FLAGS[ch] |= IS_ALPHA; - } - for (int ch = 'a'; ch <= 'z'; ch++) { - FLAGS[ch] |= IS_ALPHA; - } } @@ -100,8 +100,8 @@ public List process() { pushCharToken(TokenKind.PLUS); } break; - case '_': // the other way to start an identifier - lexIdentifier(); + case '_': + lexIdentifier(); // '_' is another way to start an identifier break; case '-': if (isTwoCharToken(TokenKind.DEC)) { @@ -213,7 +213,7 @@ else if (isTwoCharToken(TokenKind.SAFE_NAVI)) { pushPairToken(TokenKind.SELECT_LAST); } else { - lexIdentifier(); + lexIdentifier(); // '$' is another way to start an identifier } break; case '>': @@ -455,8 +455,8 @@ private void lexIdentifier() { char[] subarray = subarray(start, this.pos); // Check if this is the alternative (textual) representation of an operator (see - // alternativeOperatorNames) - if ((this.pos - start) == 2 || (this.pos - start) == 3) { + // ALTERNATIVE_OPERATOR_NAMES). + if (subarray.length == 2 || subarray.length == 3) { String asString = new String(subarray).toUpperCase(); int idx = Arrays.binarySearch(ALTERNATIVE_OPERATOR_NAMES, asString); if (idx >= 0) { @@ -569,10 +569,7 @@ private boolean isDigit(char ch) { } private boolean isAlphabetic(char ch) { - if (ch > 255) { - return false; - } - return (FLAGS[ch] & IS_ALPHA) != 0; + return Character.isLetter(ch); } private boolean isHexadecimalDigit(char ch) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java index 36dbeb10e7b4..120bb0806173 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import org.springframework.lang.Nullable; /** - * A {@link org.springframework.expression.MethodResolver} variant for data binding + * An {@link org.springframework.expression.MethodResolver} variant for data binding * purposes, using reflection to access instance methods on a given target object. * *

      This accessor does not resolve static methods and also no technical methods diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java index 6dbe01b6adec..187fb16e7f5b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.lang.reflect.Method; /** - * A {@link org.springframework.expression.PropertyAccessor} variant for data binding + * An {@link org.springframework.expression.PropertyAccessor} variant for data binding * purposes, using reflection to access properties for reading and possibly writing. * *

      A property can be referenced through a public getter method (when being read) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index e9b62ec2adbb..10854120d5dd 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.expression.spel.support; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; import java.lang.reflect.Array; import java.lang.reflect.Executable; import java.lang.reflect.Method; @@ -23,6 +25,7 @@ import java.util.Optional; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypeConverter; @@ -46,12 +49,12 @@ public abstract class ReflectionHelper { /** * Compare argument arrays and return information about whether they match. - * A supplied type converter and conversionAllowed flag allow for matches to take - * into account that a type may be transformed into a different type by the converter. + *

      The supplied type converter allows for matches to take into account that a type + * may be transformed into a different type by the converter. * @param expectedArgTypes the types the method/constructor is expecting * @param suppliedArgTypes the types that are being supplied at the point of invocation * @param typeConverter a registered type converter - * @return a MatchInfo object indicating what kind of match it was, + * @return an {@code ArgumentsMatchInfo} object indicating what kind of match it was, * or {@code null} if it was not a match */ @Nullable @@ -59,7 +62,7 @@ static ArgumentsMatchInfo compareArguments( List expectedArgTypes, List suppliedArgTypes, TypeConverter typeConverter) { Assert.isTrue(expectedArgTypes.size() == suppliedArgTypes.size(), - "Expected argument types and supplied argument types should be arrays of same length"); + "Expected argument types and supplied argument types should be lists of the same size"); ArgumentsMatchKind match = ArgumentsMatchKind.EXACT; for (int i = 0; i < expectedArgTypes.size() && match != null; i++) { @@ -71,7 +74,7 @@ static ArgumentsMatchInfo compareArguments( match = null; } } - else if (!expectedArg.equals(suppliedArg)) { + else if (!expectedArg.equals(suppliedArg)) { if (suppliedArg.isAssignableTo(expectedArg)) { if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) { match = ArgumentsMatchKind.CLOSE; @@ -133,13 +136,14 @@ else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) { /** * Compare argument arrays and return information about whether they match. - * A supplied type converter and conversionAllowed flag allow for matches to - * take into account that a type may be transformed into a different type by the - * converter. This variant of compareArguments also allows for a varargs match. + *

      The supplied type converter allows for matches to take into account that a type + * may be transformed into a different type by the converter. + *

      This variant of {@link #compareArguments(List, List, TypeConverter)} also allows + * for a varargs match. * @param expectedArgTypes the types the method/constructor is expecting * @param suppliedArgTypes the types that are being supplied at the point of invocation * @param typeConverter a registered type converter - * @return a MatchInfo object indicating what kind of match it was, + * @return an {@code ArgumentsMatchInfo} object indicating what kind of match it was, * or {@code null} if it was not a match */ @Nullable @@ -197,26 +201,26 @@ else if (typeConverter.canConvert(suppliedArg, expectedArg)) { // Now... we have the final argument in the method we are checking as a match and we have 0 // or more other arguments left to pass to it. TypeDescriptor varargsDesc = expectedArgTypes.get(expectedArgTypes.size() - 1); - TypeDescriptor elementDesc = varargsDesc.getElementTypeDescriptor(); - Assert.state(elementDesc != null, "No element type"); - Class varargsParamType = elementDesc.getType(); + TypeDescriptor componentTypeDesc = varargsDesc.getElementTypeDescriptor(); + Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array"); + Class varargsComponentType = componentTypeDesc.getType(); // All remaining parameters must be of this type or convertible to this type for (int i = expectedArgTypes.size() - 1; i < suppliedArgTypes.size(); i++) { TypeDescriptor suppliedArg = suppliedArgTypes.get(i); if (suppliedArg == null) { - if (varargsParamType.isPrimitive()) { + if (varargsComponentType.isPrimitive()) { match = null; } } else { - if (varargsParamType != suppliedArg.getType()) { - if (ClassUtils.isAssignable(varargsParamType, suppliedArg.getType())) { + if (varargsComponentType != suppliedArg.getType()) { + if (ClassUtils.isAssignable(varargsComponentType, suppliedArg.getType())) { if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) { match = ArgumentsMatchKind.CLOSE; } } - else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsParamType))) { + else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsComponentType))) { match = ArgumentsMatchKind.REQUIRES_CONVERSION; } else { @@ -231,19 +235,22 @@ else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsPar } - // TODO could do with more refactoring around argument handling and varargs /** - * Convert a supplied set of arguments into the requested types. If the parameterTypes are related to - * a varargs method then the final entry in the parameterTypes array is going to be an array itself whose - * component type should be used as the conversion target for extraneous arguments. (For example, if the - * parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both - * the boolean and float must be converted to strings). This method does *not* repackage the arguments - * into a form suitable for the varargs invocation - a subsequent call to setupArgumentsForVarargsInvocation handles that. + * Convert the supplied set of arguments into the parameter types of the supplied + * {@link Method}. + *

      If the supplied method is a varargs method, the final parameter type must be an + * array whose component type should be used as the conversion target for extraneous + * arguments. For example, if the parameter types are {Integer, String[]} + * and the input arguments are {Integer, boolean, float}, then both the + * {@code boolean} and the {@code float} must be converted to strings. + *

      This method does not repackage the arguments into a form suitable + * for the varargs invocation: a subsequent call to + * {@link #setupArgumentsForVarargsInvocation(Class[], Object...)} is required for that. * @param converter the converter to use for type conversions - * @param arguments the arguments to convert to the requested parameter types - * @param method the target Method - * @return true if some kind of conversion occurred on the argument - * @throws SpelEvaluationException if there is a problem with conversion + * @param arguments the arguments to convert to the required parameter types + * @param method the target {@code Method} + * @return {@code true} if some kind of conversion occurred on an argument + * @throws SpelEvaluationException if a problem occurs during conversion */ public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method) throws SpelEvaluationException { @@ -253,11 +260,12 @@ public static boolean convertAllArguments(TypeConverter converter, Object[] argu } /** - * Takes an input set of argument values and converts them to the types specified as the - * required parameter types. The arguments are converted 'in-place' in the input array. - * @param converter the type converter to use for attempting conversions - * @param arguments the actual arguments that need conversion - * @param executable the target Method or Constructor + * Convert the supplied set of arguments into the parameter types of the supplied + * {@link Executable}, taking the varargs position into account. + *

      The arguments are converted 'in-place' in the input array. + * @param converter the converter to use for type conversions + * @param arguments the arguments to convert to the required parameter types + * @param executable the target {@code Method} or {@code Constructor} * @param varargsPosition the known position of the varargs argument, if any * ({@code null} if not varargs) * @return {@code true} if some kind of conversion occurred on an argument @@ -271,7 +279,8 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe for (int i = 0; i < arguments.length; i++) { TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forExecutable(executable, i)); Object argument = arguments[i]; - arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + arguments[i] = converter.convertValue(argument, sourceType, targetType); conversionOccurred |= (argument != arguments[i]); } } @@ -280,35 +289,45 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe for (int i = 0; i < varargsPosition; i++) { TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forExecutable(executable, i)); Object argument = arguments[i]; - arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + arguments[i] = converter.convertValue(argument, sourceType, targetType); conversionOccurred |= (argument != arguments[i]); } MethodParameter methodParam = MethodParameter.forExecutable(executable, varargsPosition); + TypeDescriptor targetType = new TypeDescriptor(methodParam); + TypeDescriptor componentTypeDesc = targetType.getElementTypeDescriptor(); + Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array"); // If the target is varargs and there is just one more argument, then convert it here. if (varargsPosition == arguments.length - 1) { Object argument = arguments[varargsPosition]; - TypeDescriptor targetType = new TypeDescriptor(methodParam); TypeDescriptor sourceType = TypeDescriptor.forObject(argument); if (argument == null) { // Perform the equivalent of GenericConversionService.convertNullSource() for a single argument. - if (targetType.getElementTypeDescriptor().getObjectType() == Optional.class) { + if (componentTypeDesc.getObjectType() == Optional.class) { arguments[varargsPosition] = Optional.empty(); conversionOccurred = true; } } - // If the argument type is equal to the varargs element type, there is no need to - // convert it or wrap it in an array. For example, using StringToArrayConverter to - // convert a String containing a comma would result in the String being split and - // repackaged in an array when it should be used as-is. - else if (!sourceType.equals(targetType.getElementTypeDescriptor())) { - arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType); + // If the argument type is assignable to the varargs component type, there is no need to + // convert it or wrap it in an array. For example, using StringToArrayConverter to convert + // a String containing a comma would result in the String being split and repackaged in an + // array when it should be used as-is. Similarly, if the argument is an array that is + // assignable to the varargs array type, there is no need to convert it. However, if the + // argument is a java.util.List, we let the TypeConverter convert the list to an array. + else if (!sourceType.isAssignableTo(componentTypeDesc) || + (sourceType.isArray() && !sourceType.isAssignableTo(targetType)) || + (argument instanceof List)) { + + TypeDescriptor targetTypeToUse = + (sourceType.isArray() || argument instanceof List ? targetType : componentTypeDesc); + arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse); } // Possible outcomes of the above if-else block: // 1) the input argument was null, and nothing was done. - // 2) the input argument was null; the varargs element type is Optional; and the argument was converted to Optional.empty(). - // 3) the input argument was correct type but not wrapped in an array, and nothing was done. + // 2) the input argument was null; the varargs component type is Optional; and the argument was converted to Optional.empty(). + // 3) the input argument was the correct type but not wrapped in an array, and nothing was done. // 4) the input argument was already compatible (i.e., array of valid type), and nothing was done. // 5) the input argument was the wrong type and got converted and wrapped in an array. if (argument != arguments[varargsPosition] && @@ -316,13 +335,114 @@ else if (!sourceType.equals(targetType.getElementTypeDescriptor())) { conversionOccurred = true; // case 5 } } - // Otherwise, convert remaining arguments to the varargs element type. + // Otherwise, convert remaining arguments to the varargs component type. + else { + for (int i = varargsPosition; i < arguments.length; i++) { + Object argument = arguments[i]; + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + arguments[i] = converter.convertValue(argument, sourceType, componentTypeDesc); + conversionOccurred |= (argument != arguments[i]); + } + } + } + return conversionOccurred; + } + + /** + * Convert the supplied set of arguments into the parameter types of the supplied + * {@link MethodHandle}, taking the varargs position into account. + *

      The arguments are converted 'in-place' in the input array. + * @param converter the converter to use for type conversions + * @param arguments the arguments to convert to the required parameter types + * @param methodHandle the target {@code MethodHandle} + * @param varargsPosition the known position of the varargs argument, if any + * ({@code null} if not varargs) + * @return {@code true} if some kind of conversion occurred on an argument + * @throws EvaluationException if a problem occurs during conversion + * @since 6.1 + */ + public static boolean convertAllMethodHandleArguments(TypeConverter converter, Object[] arguments, + MethodHandle methodHandle, @Nullable Integer varargsPosition) throws EvaluationException { + + boolean conversionOccurred = false; + MethodType methodHandleType = methodHandle.type(); + if (varargsPosition == null) { + for (int i = 0; i < arguments.length; i++) { + Class argumentClass = methodHandleType.parameterType(i); + ResolvableType resolvableType = ResolvableType.forClass(argumentClass); + TypeDescriptor targetType = new TypeDescriptor(resolvableType, argumentClass, null); + + Object argument = arguments[i]; + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + arguments[i] = converter.convertValue(argument, sourceType, targetType); + conversionOccurred |= (argument != arguments[i]); + } + } + else { + // Convert everything up to the varargs position + for (int i = 0; i < varargsPosition; i++) { + Class argumentClass = methodHandleType.parameterType(i); + ResolvableType resolvableType = ResolvableType.forClass(argumentClass); + TypeDescriptor targetType = new TypeDescriptor(resolvableType, argumentClass, null); + + Object argument = arguments[i]; + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + arguments[i] = converter.convertValue(argument, sourceType, targetType); + conversionOccurred |= (argument != arguments[i]); + } + + Class varargsArrayClass = methodHandleType.lastParameterType(); + // We use the wrapper type for a primitive varargs array, since we eventually + // need an Object array in order to invoke the MethodHandle in + // FunctionReference#executeFunctionViaMethodHandle(). + Class varargsComponentClass = ClassUtils.resolvePrimitiveIfNecessary(varargsArrayClass.componentType()); + TypeDescriptor varargsArrayType = TypeDescriptor.array(TypeDescriptor.valueOf(varargsComponentClass)); + Assert.state(varargsArrayType != null, "Array type must not be null for a varargs array"); + TypeDescriptor varargsComponentType = varargsArrayType.getElementTypeDescriptor(); + Assert.state(varargsComponentType != null, "Component type must not be null for a varargs array"); + + // If the target is varargs and there is just one more argument, then convert it here. + if (varargsPosition == arguments.length - 1) { + Object argument = arguments[varargsPosition]; + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + if (argument == null) { + // Perform the equivalent of GenericConversionService.convertNullSource() for a single argument. + if (varargsComponentType.getObjectType() == Optional.class) { + arguments[varargsPosition] = Optional.empty(); + conversionOccurred = true; + } + } + // If the argument type is assignable to the varargs component type, there is no need to + // convert it. For example, using StringToArrayConverter to convert a String containing a + // comma would result in the String being split and repackaged in an array when it should + // be used as-is. Similarly, if the argument is an array that is assignable to the varargs + // array type, there is no need to convert it. However, if the argument is a java.util.List, + // we let the TypeConverter convert the list to an array. + else if (!sourceType.isAssignableTo(varargsComponentType) || + (sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType)) || + (argument instanceof List)) { + + TypeDescriptor targetTypeToUse = + (sourceType.isArray() || argument instanceof List ? varargsArrayType : varargsComponentType); + arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse); + } + // Possible outcomes of the above if-else block: + // 1) the input argument was null, and nothing was done. + // 2) the input argument was null; the varargs component type is Optional; and the argument was converted to Optional.empty(). + // 3) the input argument was the correct type but not wrapped in an array, and nothing was done. + // 4) the input argument was already compatible (i.e., an Object array of valid type), and nothing was done. + // 5) the input argument was the wrong type and got converted as explained in the comments above. + if (argument != arguments[varargsPosition] && + !isFirstEntryInArray(argument, arguments[varargsPosition])) { + conversionOccurred = true; // case 5 + } + } + // Otherwise, convert remaining arguments to the varargs component type. else { - TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor(); - Assert.state(targetType != null, "No element type"); for (int i = varargsPosition; i < arguments.length; i++) { Object argument = arguments[i]; - arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + arguments[i] = converter.convertValue(argument, sourceType, varargsComponentType); conversionOccurred |= (argument != arguments[i]); } } @@ -342,32 +462,37 @@ private static boolean isFirstEntryInArray(Object value, @Nullable Object possib } Class type = possibleArray.getClass(); if (!type.isArray() || Array.getLength(possibleArray) == 0 || - !ClassUtils.isAssignableValue(type.getComponentType(), value)) { + !ClassUtils.isAssignableValue(type.componentType(), value)) { return false; } Object arrayValue = Array.get(possibleArray, 0); - return (type.getComponentType().isPrimitive() ? arrayValue.equals(value) : arrayValue == value); + return (type.componentType().isPrimitive() ? arrayValue.equals(value) : arrayValue == value); } /** - * Package up the arguments so that they correctly match what is expected in requiredParameterTypes. - *

      For example, if requiredParameterTypes is {@code (int, String[])} because the second parameter - * was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be - * repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types. + * Package up the supplied {@code args} so that they correctly match what is + * expected in {@code requiredParameterTypes}. + *

      For example, if {@code requiredParameterTypes} is {@code (int, String[])} + * because the second parameter was declared as {@code String...}, then if + * {@code args} is {@code [1, "a", "b"]} it must be repackaged as + * {@code [1, new String[] {"a", "b"}]} in order to match the expected types. * @param requiredParameterTypes the types of the parameters for the invocation - * @param args the arguments to be setup ready for the invocation - * @return a repackaged array of arguments where any varargs setup has been done + * @param args the arguments to be set up for the invocation + * @return a repackaged array of arguments where any varargs setup has been performed */ public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredParameterTypes, Object... args) { - // Check if array already built for final argument + Assert.notEmpty(requiredParameterTypes, "Required parameter types array must not be empty"); + int parameterCount = requiredParameterTypes.length; + Class lastRequiredParameterType = requiredParameterTypes[parameterCount - 1]; + Assert.isTrue(lastRequiredParameterType.isArray(), + "The last required parameter type must be an array to support varargs invocation"); + int argumentCount = args.length; + Object lastArgument = (argumentCount > 0 ? args[argumentCount - 1] : null); // Check if repackaging is needed... - if (parameterCount != args.length || - requiredParameterTypes[parameterCount - 1] != - (args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) { - + if (parameterCount != argumentCount || !lastRequiredParameterType.isInstance(lastArgument)) { // Create an array for the leading arguments plus the varargs array argument. Object[] newArgs = new Object[parameterCount]; // Copy all leading arguments to the new array, omitting the varargs array argument. @@ -379,7 +504,7 @@ public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredPar if (argumentCount >= parameterCount) { varargsArraySize = argumentCount - (parameterCount - 1); } - Class componentType = requiredParameterTypes[parameterCount - 1].getComponentType(); + Class componentType = lastRequiredParameterType.componentType(); Object varargsArray = Array.newInstance(componentType, varargsArraySize); for (int i = 0; i < varargsArraySize; i++) { Array.set(varargsArray, i, args[parameterCount - 1 + i]); @@ -388,6 +513,7 @@ public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredPar newArgs[newArgs.length - 1] = varargsArray; return newArgs; } + return args; } @@ -409,19 +535,13 @@ enum ArgumentsMatchKind { /** - * An instance of ArgumentsMatchInfo describes what kind of match was achieved + * An instance of {@code ArgumentsMatchInfo} describes what kind of match was achieved * between two sets of arguments - the set that a method/constructor is expecting - * and the set that are being supplied at the point of invocation. If the kind - * indicates that conversion is required for some of the arguments then the arguments - * that require conversion are listed in the argsRequiringConversion array. + * and the set that is being supplied at the point of invocation. + * + * @param kind the kind of match that was achieved */ - static class ArgumentsMatchInfo { - - private final ArgumentsMatchKind kind; - - ArgumentsMatchInfo(ArgumentsMatchKind kind) { - this.kind = kind; - } + record ArgumentsMatchInfo(ArgumentsMatchKind kind) { public boolean isExactMatch() { return (this.kind == ArgumentsMatchKind.EXACT); @@ -437,7 +557,7 @@ public boolean isMatchRequiringConversion() { @Override public String toString() { - return "ArgumentMatchInfo: " + this.kind; + return "ArgumentsMatchInfo: " + this.kind; } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java index 8ec11fabecb7..ba55a8ae83ef 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,8 @@ import org.springframework.lang.Nullable; /** - * A constructor resolver that uses reflection to locate the constructor that should be invoked. + * A constructor resolver that uses reflection to locate the constructor that + * should be invoked. * * @author Andy Clement * @author Juergen Hoeller @@ -42,12 +43,15 @@ public class ReflectiveConstructorResolver implements ConstructorResolver { /** - * Locate a constructor on the type. There are three kinds of match that might occur: + * Locate a constructor on the type. + *

      There are three kinds of matches that might occur: *

        - *
      1. An exact match where the types of the arguments match the types of the constructor - *
      2. An in-exact match where the types we are looking for are subtypes of those defined on the constructor - *
      3. A match where we are able to convert the arguments into those expected by the constructor, according to the - * registered type converter. + *
      4. An exact match where the types of the arguments match the types of the + * constructor.
      5. + *
      6. An inexact match where the types we are looking for are subtypes of + * those defined on the constructor.
      7. + *
      8. A match where we are able to convert the arguments into those expected + * by the constructor, according to the registered type converter.
      9. *
      */ @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java index 5e4b50187f8c..9de4917c0d57 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,12 +87,15 @@ public final Method getMethod() { } /** - * Find the first public class in the methods declaring class hierarchy that declares this method. - * Sometimes the reflective method discovery logic finds a suitable method that can easily be - * called via reflection but cannot be called from generated code when compiling the expression - * because of visibility restrictions. For example if a non-public class overrides toString(), - * this helper method will walk up the type hierarchy to find the first public type that declares - * the method (if there is one!). For toString() it may walk as far as Object. + * Find the first public class in the method's declaring class hierarchy that + * declares this method. + *

      Sometimes the reflective method discovery logic finds a suitable method + * that can easily be called via reflection but cannot be called from generated + * code when compiling the expression because of visibility restrictions. For + * example, if a non-public class overrides {@code toString()}, this helper + * method will traverse up the type hierarchy to find the first public type that + * declares the method (if there is one). For {@code toString()}, it may traverse + * as far as Object. */ @Nullable public Class getPublicDeclaringClass() { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java index 5d756d07d4e2..dbf696c13a2e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,7 @@ public class ReflectiveMethodResolver implements MethodResolver { public ReflectiveMethodResolver() { - this.useDistance = true; + this(true); } /** @@ -100,12 +100,15 @@ public void registerMethodFilter(Class type, @Nullable MethodFilter filter) { } /** - * Locate a method on a type. There are three kinds of match that might occur: + * Locate a method on the type. + *

      There are three kinds of matches that might occur: *

        - *
      1. an exact match where the types of the arguments match the types of the constructor - *
      2. an in-exact match where the types we are looking for are subtypes of those defined on the constructor - *
      3. a match where we are able to convert the arguments into those expected by the constructor, - * according to the registered type converter + *
      4. An exact match where the types of the arguments match the types of the + * method.
      5. + *
      6. An inexact match where the types we are looking for are subtypes of + * those defined on the method.
      7. + *
      8. A match where we are able to convert the arguments into those expected + * by the method, according to the registered type converter.
      9. *
      */ @Override @@ -117,6 +120,7 @@ public MethodExecutor resolve(EvaluationContext context, Object targetObject, St TypeConverter typeConverter = context.getTypeConverter(); Class type = (targetObject instanceof Class clazz ? clazz : targetObject.getClass()); ArrayList methods = new ArrayList<>(getMethods(type, targetObject)); + methods.removeIf(method -> !method.getName().equals(name)); // If a filter is registered for this type, call it MethodFilter filter = (this.filters != null ? this.filters.get(type) : null); @@ -160,48 +164,46 @@ else if (m1.isVarArgs() && !m2.isVarArgs()) { boolean multipleOptions = false; for (Method method : methodsToIterate) { - if (method.getName().equals(name)) { - int paramCount = method.getParameterCount(); - List paramDescriptors = new ArrayList<>(paramCount); - for (int i = 0; i < paramCount; i++) { - paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i))); - } - ReflectionHelper.ArgumentsMatchInfo matchInfo = null; - if (method.isVarArgs() && argumentTypes.size() >= (paramCount - 1)) { - // *sigh* complicated - matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); - } - else if (paramCount == argumentTypes.size()) { - // Name and parameter number match, check the arguments - matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + int paramCount = method.getParameterCount(); + List paramDescriptors = new ArrayList<>(paramCount); + for (int i = 0; i < paramCount; i++) { + paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i))); + } + ReflectionHelper.ArgumentsMatchInfo matchInfo = null; + if (method.isVarArgs() && argumentTypes.size() >= (paramCount - 1)) { + // *sigh* complicated + matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); + } + else if (paramCount == argumentTypes.size()) { + // Name and parameter number match, check the arguments + matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + } + if (matchInfo != null) { + if (matchInfo.isExactMatch()) { + return new ReflectiveMethodExecutor(method, type); } - if (matchInfo != null) { - if (matchInfo.isExactMatch()) { - return new ReflectiveMethodExecutor(method, type); - } - else if (matchInfo.isCloseMatch()) { - if (this.useDistance) { - int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes); - if (closeMatch == null || matchDistance < closeMatchDistance) { - // This is a better match... - closeMatch = method; - closeMatchDistance = matchDistance; - } - } - else { - // Take this as a close match if there isn't one already - if (closeMatch == null) { - closeMatch = method; - } + else if (matchInfo.isCloseMatch()) { + if (this.useDistance) { + int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes); + if (closeMatch == null || matchDistance < closeMatchDistance) { + // This is a better match... + closeMatch = method; + closeMatchDistance = matchDistance; } } - else if (matchInfo.isMatchRequiringConversion()) { - if (matchRequiringConversion != null) { - multipleOptions = true; + else { + // Take this as a close match if there isn't one already + if (closeMatch == null) { + closeMatch = method; } - matchRequiringConversion = method; } } + else if (matchInfo.isMatchRequiringConversion()) { + if (matchRequiringConversion != null) { + multipleOptions = true; + } + matchRequiringConversion = method; + } } } if (closeMatch != null) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index fa2cd34fbc41..632860a4a377 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,15 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; +import kotlin.reflect.KMutableProperty; +import kotlin.reflect.KProperty; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.jvm.ReflectJvmMapping; + import org.springframework.asm.MethodVisitor; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.convert.Property; import org.springframework.core.convert.TypeDescriptor; @@ -55,6 +63,7 @@ * @author Juergen Hoeller * @author Phillip Webb * @author Sam Brannen + * @author Sebastien Deleuze * @since 3.0 * @see StandardEvaluationContext * @see SimpleEvaluationContext @@ -64,7 +73,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { private static final Set> ANY_TYPES = Collections.emptySet(); - private static final Set> BOOLEAN_TYPES = Set.of(Boolean.class, Boolean.TYPE); + private static final Set> BOOLEAN_TYPES = Set.of(Boolean.class, boolean.class); private final boolean allowWrite; @@ -329,7 +338,7 @@ private TypeDescriptor getTypeDescriptor(EvaluationContext context, Object targe Class type = (target instanceof Class clazz ? clazz : target.getClass()); if (type.isArray() && name.equals("length")) { - return TypeDescriptor.valueOf(Integer.TYPE); + return TypeDescriptor.valueOf(int.class); } PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); TypeDescriptor typeDescriptor = this.typeDescriptorCache.get(cacheKey); @@ -401,7 +410,8 @@ private Method findMethodForProperty(String[] methodSuffixes, String prefix, Cla Method[] methods = getSortedMethods(clazz); for (String methodSuffix : methodSuffixes) { for (Method method : methods) { - if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) && + if (isCandidateForProperty(method, clazz) && + (method.getName().equals(prefix + methodSuffix) || isKotlinProperty(method, methodSuffix)) && method.getParameterCount() == numberOfParams && (!mustBeStatic || Modifier.isStatic(method.getModifiers())) && (requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) { @@ -557,23 +567,19 @@ public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullab return this; } + private static boolean isKotlinProperty(Method method, String methodSuffix) { + Class clazz = method.getDeclaringClass(); + return KotlinDetector.isKotlinReflectPresent() && + KotlinDetector.isKotlinType(clazz) && + KotlinDelegate.isKotlinProperty(method, methodSuffix); + } + /** * Captures the member (method/field) to call reflectively to access a property value * and the type descriptor for the value returned by the reflective call. */ - private static class InvokerPair { - - final Member member; - - final TypeDescriptor typeDescriptor; - - public InvokerPair(Member member, TypeDescriptor typeDescriptor) { - this.member = member; - this.typeDescriptor = typeDescriptor; - } - } - + private record InvokerPair(Member member, TypeDescriptor typeDescriptor) {} private static final class PropertyCacheKey implements Comparable { @@ -755,4 +761,24 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { } } + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + public static boolean isKotlinProperty(Method method, String methodSuffix) { + KClass kClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass()); + for (KProperty property : KClasses.getMemberProperties(kClass)) { + if (methodSuffix.equalsIgnoreCase(property.getName()) && + (method.equals(ReflectJvmMapping.getJavaGetter(property)) || + property instanceof KMutableProperty mutableProperty && + method.equals(ReflectJvmMapping.getJavaSetter(mutableProperty)))) { + return true; + } + } + return false; + } + + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index 1168c9c91a26..1b4e5c205a51 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,29 +51,38 @@ * SpEL language syntax, e.g. excluding references to Java types, constructors, * and bean references. * - *

      When creating a {@code SimpleEvaluationContext} you need to choose the - * level of support that you need for property access in SpEL expressions: + *

      When creating a {@code SimpleEvaluationContext} you need to choose the level of + * support that you need for data binding in SpEL expressions: *

        - *
      • A custom {@code PropertyAccessor} (typically not reflection-based), - * potentially combined with a {@link DataBindingPropertyAccessor}
      • - *
      • Data binding properties for read-only access
      • - *
      • Data binding properties for read and write
      • + *
      • Data binding for read-only access
      • + *
      • Data binding for read and write access
      • + *
      • A custom {@code PropertyAccessor} (typically not reflection-based), potentially + * combined with a {@link DataBindingPropertyAccessor}
      • *
      * - *

      Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} - * enables read access to properties via {@link DataBindingPropertyAccessor}; - * same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when - * write access is needed as well. Alternatively, configure custom accessors - * via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially + *

      Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} enables + * read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly, + * {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access + * to properties. Alternatively, configure custom accessors via + * {@link SimpleEvaluationContext#forPropertyAccessors}, potentially + * {@linkplain Builder#withAssignmentDisabled() disable assignment}, and optionally * activate method resolution and/or a type converter through the builder. * *

      Note that {@code SimpleEvaluationContext} is typically not configured * with a default root object. Instead it is meant to be created once and - * used repeatedly through {@code getValue} calls on a pre-compiled + * used repeatedly through {@code getValue} calls on a predefined * {@link org.springframework.expression.Expression} with both an * {@code EvaluationContext} and a root object as arguments: * {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}. * + *

      In addition to support for setting and looking up variables as defined in + * the {@link EvaluationContext} API, {@code SimpleEvaluationContext} also + * provides support for {@linkplain #setVariable(String, Object) registering} and + * {@linkplain #lookupVariable(String) looking up} functions as variables. Since + * functions share a common namespace with the variables in this evaluation + * context, care must be taken to ensure that function names and variable names + * do not overlap. + * *

      For more power and flexibility, in particular for internal configuration * scenarios, consider using {@link StandardEvaluationContext} instead. * @@ -81,12 +90,13 @@ * @author Juergen Hoeller * @author Sam Brannen * @since 4.3.15 - * @see #forPropertyAccessors * @see #forReadOnlyDataBinding() * @see #forReadWriteDataBinding() + * @see #forPropertyAccessors * @see StandardEvaluationContext * @see StandardTypeConverter * @see DataBindingPropertyAccessor + * @see DataBindingMethodResolver */ public final class SimpleEvaluationContext implements EvaluationContext { @@ -103,20 +113,24 @@ public final class SimpleEvaluationContext implements EvaluationContext { private final TypeConverter typeConverter; - private final TypeComparator typeComparator = new StandardTypeComparator(); + private final TypeComparator typeComparator = StandardTypeComparator.INSTANCE; - private final OperatorOverloader operatorOverloader = new StandardOperatorOverloader(); + private final OperatorOverloader operatorOverloader = StandardOperatorOverloader.INSTANCE; private final Map variables = new HashMap<>(); + private final boolean assignmentEnabled; + + private SimpleEvaluationContext(List accessors, List resolvers, - @Nullable TypeConverter converter, @Nullable TypedValue rootObject) { + @Nullable TypeConverter converter, @Nullable TypedValue rootObject, boolean assignmentEnabled) { this.propertyAccessors = accessors; this.methodResolvers = resolvers; this.typeConverter = (converter != null ? converter : new StandardTypeConverter()); this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL); + this.assignmentEnabled = assignmentEnabled; } @@ -168,7 +182,7 @@ public BeanResolver getBeanResolver() { /** * {@code SimpleEvaluationContext} does not support use of type references. * @return {@code TypeLocator} implementation that raises a - * {@link SpelEvaluationException} with {@link SpelMessage#TYPE_NOT_FOUND}. + * {@link SpelEvaluationException} with {@link SpelMessage#TYPE_NOT_FOUND} */ @Override public TypeLocator getTypeLocator() { @@ -213,26 +227,66 @@ public TypedValue assignVariable(String name, Supplier valueSupplier throw new SpelEvaluationException(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED, "#" + name); } + /** + * Set a named variable or function in this evaluation context to the specified + * value. + *

      A function can be registered as a {@link java.lang.reflect.Method} or + * a {@link java.lang.invoke.MethodHandle}. + *

      Note that variables and functions share a common namespace in this + * evaluation context. See the {@linkplain SimpleEvaluationContext + * class-level documentation} for details. + * @param name the name of the variable or function to set + * @param value the value to be placed in the variable or function + * @see #lookupVariable(String) + */ @Override public void setVariable(String name, @Nullable Object value) { this.variables.put(name, value); } + /** + * Look up a named variable or function within this evaluation context. + *

      Note that variables and functions share a common namespace in this + * evaluation context. See the {@linkplain SimpleEvaluationContext + * class-level documentation} for details. + * @param name the name of the variable or function to look up + * @return the value of the variable or function, or {@code null} if not found + */ @Override @Nullable public Object lookupVariable(String name) { return this.variables.get(name); } + /** + * Determine if assignment is enabled within expressions evaluated by this evaluation + * context. + *

      If this method returns {@code false}, the assignment ({@code =}), increment + * ({@code ++}), and decrement ({@code --}) operators are disabled. + * @return {@code true} if assignment is enabled; {@code false} otherwise + * @since 5.3.38 + * @see #forReadOnlyDataBinding() + * @see Builder#withAssignmentDisabled() + */ + @Override + public boolean isAssignmentEnabled() { + return this.assignmentEnabled; + } /** * Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor} - * delegates: typically a custom {@code PropertyAccessor} specific to a use case - * (e.g. attribute resolution in a custom data structure), potentially combined with - * a {@link DataBindingPropertyAccessor} if property dereferences are needed as well. + * delegates: typically a custom {@code PropertyAccessor} specific to a use case — + * for example, for attribute resolution in a custom data structure — potentially + * combined with a {@link DataBindingPropertyAccessor} if property dereferences are + * needed as well. + *

      By default, assignment is enabled within expressions evaluated by the context + * created via this factory method; however, assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}. * @param accessors the accessor delegates to use * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see DataBindingPropertyAccessor#forReadWriteAccess() + * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { for (PropertyAccessor accessor : accessors) { @@ -247,18 +301,28 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { /** * Create a {@code SimpleEvaluationContext} for read-only access to * public properties via {@link DataBindingPropertyAccessor}. + *

      Assignment is disabled within expressions evaluated by the context created via + * this factory method. * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see #forPropertyAccessors + * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadOnlyDataBinding() { - return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()); + return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()).withAssignmentDisabled(); } /** * Create a {@code SimpleEvaluationContext} for read-write access to * public properties via {@link DataBindingPropertyAccessor}. + *

      By default, assignment is enabled within expressions evaluated by the context + * created via this factory method. Assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}; however, it is preferable to use + * {@link #forReadOnlyDataBinding()} if you desire read-only access. * @see DataBindingPropertyAccessor#forReadWriteAccess() * @see #forPropertyAccessors + * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadWriteDataBinding() { return new Builder(DataBindingPropertyAccessor.forReadWriteAccess()); @@ -268,7 +332,7 @@ public static Builder forReadWriteDataBinding() { /** * Builder for {@code SimpleEvaluationContext}. */ - public static class Builder { + public static final class Builder { private final List accessors; @@ -280,10 +344,22 @@ public static class Builder { @Nullable private TypedValue rootObject; - public Builder(PropertyAccessor... accessors) { + private boolean assignmentEnabled = true; + + private Builder(PropertyAccessor... accessors) { this.accessors = Arrays.asList(accessors); } + /** + * Disable assignment within expressions evaluated by this evaluation context. + * @since 5.3.38 + * @see SimpleEvaluationContext#isAssignmentEnabled() + */ + public Builder withAssignmentDisabled() { + this.assignmentEnabled = false; + return this; + } + /** * Register the specified {@link MethodResolver} delegates for * a combination of property access and method resolution. @@ -315,7 +391,6 @@ public Builder withInstanceMethods() { return this; } - /** * Register a custom {@link ConversionService}. *

      By default a {@link StandardTypeConverter} backed by a @@ -327,6 +402,7 @@ public Builder withConversionService(ConversionService conversionService) { this.typeConverter = new StandardTypeConverter(conversionService); return this; } + /** * Register a custom {@link TypeConverter}. *

      By default a {@link StandardTypeConverter} backed by a @@ -362,7 +438,8 @@ public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescrip } public SimpleEvaluationContext build() { - return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject); + return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject, + this.assignmentEnabled); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index daac3a23f2ac..5df60b3bd0ef 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.expression.spel.support; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -47,6 +48,16 @@ * to reliably locate user types. See {@link #setTypeLocator(TypeLocator)} for * details. * + *

      In addition to support for setting and looking up variables as defined in + * the {@link EvaluationContext} API, {@code StandardEvaluationContext} also + * provides support for registering and looking up functions. The + * {@code registerFunction(...)} methods provide a convenient way to register a + * function as a {@link Method} or a {@link MethodHandle}; however, a function + * can also be registered via {@link #setVariable(String, Object)} or + * {@link #setVariables(Map)}. Since functions share a namespace with the variables + * in this evaluation context, care must be taken to ensure that function names + * and variable names do not overlap. + * *

      For a simpler, builder-style context variant for data-binding purposes, * consider using {@link SimpleEvaluationContext} instead which allows for * opting into several SpEL features as needed by specific use cases. @@ -54,6 +65,7 @@ * @author Andy Clement * @author Juergen Hoeller * @author Sam Brannen + * @author Stephane Nicoll * @since 3.0 * @see SimpleEvaluationContext * @see ReflectivePropertyAccessor @@ -89,9 +101,9 @@ public class StandardEvaluationContext implements EvaluationContext { @Nullable private TypeConverter typeConverter; - private TypeComparator typeComparator = new StandardTypeComparator(); + private TypeComparator typeComparator = StandardTypeComparator.INSTANCE; - private OperatorOverloader operatorOverloader = new StandardOperatorOverloader(); + private OperatorOverloader operatorOverloader = StandardOperatorOverloader.INSTANCE; private final Map variables = new ConcurrentHashMap<>(); @@ -251,6 +263,25 @@ public OperatorOverloader getOperatorOverloader() { return this.operatorOverloader; } + /** + * Set a named variable in this evaluation context to a specified value. + *

      If the specified {@code name} is {@code null}, it will be ignored. If + * the specified {@code value} is {@code null}, the named variable will be + * removed from this evaluation context. + *

      In contrast to {@link #assignVariable(String,java.util.function.Supplier)}, + * this method should only be invoked programmatically when interacting directly + * with the {@code EvaluationContext} — for example, to provide initial + * configuration for the context. + *

      Note that variables and functions share a common namespace in this + * evaluation context. See the {@linkplain StandardEvaluationContext + * class-level documentation} for details. + * @param name the name of the variable to set + * @param value the value to be placed in the variable + * @see #setVariables(Map) + * @see #registerFunction(String, Method) + * @see #registerFunction(String, MethodHandle) + * @see #lookupVariable(String) + */ @Override public void setVariable(@Nullable String name, @Nullable Object value) { // For backwards compatibility, we ignore null names here... @@ -266,14 +297,54 @@ public void setVariable(@Nullable String name, @Nullable Object value) { } } + /** + * Set multiple named variables in this evaluation context to the specified values. + *

      This is a convenience variant of {@link #setVariable(String, Object)}. + *

      Note that variables and functions share a common namespace in this + * evaluation context. See the {@linkplain StandardEvaluationContext + * class-level documentation} for details. + * @param variables the names and values of the variables to set + * @see #setVariable(String, Object) + */ public void setVariables(Map variables) { variables.forEach(this::setVariable); } + /** + * Register the specified {@link Method} as a SpEL function. + *

      Note that variables and functions share a common namespace in this + * evaluation context. See the {@linkplain StandardEvaluationContext + * class-level documentation} for details. + * @param name the name of the function + * @param method the {@code Method} to register + * @see #registerFunction(String, MethodHandle) + */ public void registerFunction(String name, Method method) { this.variables.put(name, method); } + /** + * Register the specified {@link MethodHandle} as a SpEL function. + *

      Note that variables and functions share a common namespace in this + * evaluation context. See the {@linkplain StandardEvaluationContext + * class-level documentation} for details. + * @param name the name of the function + * @param methodHandle the {@link MethodHandle} to register + * @since 6.1 + * @see #registerFunction(String, Method) + */ + public void registerFunction(String name, MethodHandle methodHandle) { + this.variables.put(name, methodHandle); + } + + /** + * Look up a named variable or function within this evaluation context. + *

      Note that variables and functions share a common namespace in this + * evaluation context. See the {@linkplain StandardEvaluationContext + * class-level documentation} for details. + * @param name the name of the variable or function to look up + * @return the value of the variable or function, or {@code null} if not found + */ @Override @Nullable public Object lookupVariable(String name) { @@ -299,6 +370,29 @@ public void registerMethodFilter(Class type, MethodFilter filter) throws Ille resolver.registerMethodFilter(type, filter); } + /** + * Apply the internal delegates of this instance to the specified + * {@code evaluationContext}. Typically invoked right after the new context + * instance has been created to reuse the delegates. Does not modify the + * {@linkplain #setRootObject(Object) root object} or any registered + * {@linkplain #setVariable variables or functions}. + * @param evaluationContext the evaluation context to update + * @since 6.1.1 + */ + public void applyDelegatesTo(StandardEvaluationContext evaluationContext) { + // Triggers initialization for default delegates + evaluationContext.setConstructorResolvers(new ArrayList<>(this.getConstructorResolvers())); + evaluationContext.setMethodResolvers(new ArrayList<>(this.getMethodResolvers())); + evaluationContext.setPropertyAccessors(new ArrayList<>(this.getPropertyAccessors())); + evaluationContext.setTypeLocator(this.getTypeLocator()); + evaluationContext.setTypeConverter(this.getTypeConverter()); + + evaluationContext.beanResolver = this.beanResolver; + evaluationContext.operatorOverloader = this.operatorOverloader; + evaluationContext.reflectiveMethodResolver = this.reflectiveMethodResolver; + evaluationContext.typeComparator = this.typeComparator; + } + private List initPropertyAccessors() { List accessors = this.propertyAccessors; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java index a2a1ea7310c0..7b8d892b0d7e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,8 @@ */ public class StandardOperatorOverloader implements OperatorOverloader { + static final StandardOperatorOverloader INSTANCE = new StandardOperatorOverloader(); + @Override public boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) throws EvaluationException { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java index 4ecb403c2204..fc913470dc44 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java @@ -37,6 +37,8 @@ */ public class StandardTypeComparator implements TypeComparator { + static final StandardTypeComparator INSTANCE = new StandardTypeComparator(); + @Override public boolean canCompare(@Nullable Object left, @Nullable Object right) { if (left == null || right == null) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java index 90e960de6764..816b66cb71c5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java @@ -120,9 +120,10 @@ public Class findType(String typeName) throws EvaluationException { return cachedType; } Class loadedType = loadType(typeName); - if (loadedType != null && - !(this.classLoader instanceof SmartClassLoader scl && scl.isClassReloadable(loadedType))) { - this.typeCache.put(typeName, loadedType); + if (loadedType != null) { + if (!(this.classLoader instanceof SmartClassLoader scl && scl.isClassReloadable(loadedType))) { + this.typeCache.put(typeName, loadedType); + } return loadedType; } throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java index e808aa661c5c..48e0b062112d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,7 +127,7 @@ public void evaluate(String expression, Object expectedValue, Class expectedR } assertThat(expectedValue).as("Expression returned null value, but expected '" + expectedValue + "'").isNull(); } - Class resultType = value.getClass(); + Class resultType = value.getClass(); if (expectedValue instanceof String) { assertThat(AbstractExpressionTests.stringValueOf(value)).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); } @@ -258,9 +258,9 @@ protected static String stringValueOf(Object value, boolean isNested) { } if (value.getClass().isArray()) { StringBuilder sb = new StringBuilder(); - if (value.getClass().getComponentType().isPrimitive()) { - Class primitiveType = value.getClass().getComponentType(); - if (primitiveType == Integer.TYPE) { + if (value.getClass().componentType().isPrimitive()) { + Class primitiveType = value.getClass().componentType(); + if (primitiveType == int.class) { int[] l = (int[]) value; sb.append("int[").append(l.length).append("]{"); for (int j = 0; j < l.length; j++) { @@ -271,7 +271,7 @@ protected static String stringValueOf(Object value, boolean isNested) { } sb.append('}'); } - else if (primitiveType == Long.TYPE) { + else if (primitiveType == long.class) { long[] l = (long[]) value; sb.append("long[").append(l.length).append("]{"); for (int j = 0; j < l.length; j++) { @@ -287,10 +287,10 @@ else if (primitiveType == Long.TYPE) { " in ExpressionTestCase.stringValueOf()"); } } - else if (value.getClass().getComponentType().isArray()) { + else if (value.getClass().componentType().isArray()) { List l = Arrays.asList((Object[]) value); if (!isNested) { - sb.append(value.getClass().getComponentType().getName()); + sb.append(value.getClass().componentType().getName()); } sb.append('[').append(l.size()).append("]{"); int i = 0; @@ -306,7 +306,7 @@ else if (value.getClass().getComponentType().isArray()) { else { List l = Arrays.asList((Object[]) value); if (!isNested) { - sb.append(value.getClass().getComponentType().getName()); + sb.append(value.getClass().componentType().getName()); } sb.append('[').append(l.size()).append("]{"); int i = 0; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/BooleanExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/BooleanExpressionTests.java index 9761734adec4..d129b9dfb0d4 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/BooleanExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/BooleanExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,20 +28,20 @@ * @author Andy Clement * @author Oliver Becker */ -public class BooleanExpressionTests extends AbstractExpressionTests { +class BooleanExpressionTests extends AbstractExpressionTests { @Test - public void testBooleanTrue() { + void testBooleanTrue() { evaluate("true", Boolean.TRUE, Boolean.class); } @Test - public void testBooleanFalse() { + void testBooleanFalse() { evaluate("false", Boolean.FALSE, Boolean.class); } @Test - public void testOr() { + void testOr() { evaluate("false or false", Boolean.FALSE, Boolean.class); evaluate("false or true", Boolean.TRUE, Boolean.class); evaluate("true or false", Boolean.TRUE, Boolean.class); @@ -49,7 +49,7 @@ public void testOr() { } @Test - public void testAnd() { + void testAnd() { evaluate("false and false", Boolean.FALSE, Boolean.class); evaluate("false and true", Boolean.FALSE, Boolean.class); evaluate("true and false", Boolean.FALSE, Boolean.class); @@ -57,7 +57,7 @@ public void testAnd() { } @Test - public void testNot() { + void testNot() { evaluate("!false", Boolean.TRUE, Boolean.class); evaluate("!true", Boolean.FALSE, Boolean.class); @@ -66,21 +66,21 @@ public void testNot() { } @Test - public void testCombinations01() { + void testCombinations01() { evaluate("false and false or true", Boolean.TRUE, Boolean.class); evaluate("true and false or true", Boolean.TRUE, Boolean.class); evaluate("true and false or false", Boolean.FALSE, Boolean.class); } @Test - public void testWritability() { + void testWritability() { evaluate("true and true", Boolean.TRUE, Boolean.class, false); evaluate("true or true", Boolean.TRUE, Boolean.class, false); evaluate("!false", Boolean.TRUE, Boolean.class, false); } @Test - public void testBooleanErrors01() { + void testBooleanErrors01() { evaluateAndCheckError("1.0 or false", SpelMessage.TYPE_CONVERSION_ERROR, 0); evaluateAndCheckError("false or 39.4", SpelMessage.TYPE_CONVERSION_ERROR, 9); evaluateAndCheckError("true and 'hello'", SpelMessage.TYPE_CONVERSION_ERROR, 9); @@ -90,7 +90,7 @@ public void testBooleanErrors01() { } @Test - public void testConvertAndHandleNull() { // SPR-9445 + void testConvertAndHandleNull() { // SPR-9445 // without null conversion evaluateAndCheckError("null or true", SpelMessage.TYPE_CONVERSION_ERROR, 0, "null", "boolean"); evaluateAndCheckError("null and true", SpelMessage.TYPE_CONVERSION_ERROR, 0, "null", "boolean"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java index 147faf8d349a..cca848eae21e 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ * * @author Oliver Becker */ -public class CachedMethodExecutorTests { +class CachedMethodExecutorTests { private final ExpressionParser parser = new SpelExpressionParser(); @@ -39,7 +39,7 @@ public class CachedMethodExecutorTests { @Test - public void testCachedExecutionForParameters() { + void testCachedExecutionForParameters() { Expression expression = this.parser.parseExpression("echo(#var)"); assertMethodExecution(expression, 42, "int: 42"); @@ -49,7 +49,7 @@ public void testCachedExecutionForParameters() { } @Test - public void testCachedExecutionForTarget() { + void testCachedExecutionForTarget() { Expression expression = this.parser.parseExpression("#var.echo(42)"); assertMethodExecution(expression, new RootObject(), "int: 42"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java index b4ad25766107..6aede670ad58 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,12 +32,12 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for type comparison + * Tests for type comparison * * @author Andy Clement * @author Giovanni Dall'Oglio Risso */ -public class ComparatorTests { +class ComparatorTests { @Test void testPrimitives() throws EvaluationException { @@ -126,7 +126,7 @@ void testCanCompare() throws EvaluationException { } @Test - public void customComparatorWorksWithEquality() { + void customComparatorWorksWithEquality() { final StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setTypeComparator(customComparator); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java new file mode 100644 index 000000000000..0d065f5bb298 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel; + +import java.util.Map; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * This is a local COPY of {@link org.springframework.context.expression.MapAccessor}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 4.1 + */ +public class CompilableMapAccessor implements CompilablePropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return (target instanceof Map map && map.containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof Map, "Target must be of type Map"); + Map map = (Map) target; + Object value = map.get(name); + if (value == null && !map.containsKey(name)) { + throw new MapAccessException(name); + } + return new TypedValue(value); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + Assert.state(target instanceof Map, "Target must be a Map"); + Map map = (Map) target; + map.put(name, newValue); + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + String descriptor = cf.lastDescriptor(); + if (descriptor == null || !descriptor.equals("Ljava/util/Map")) { + if (descriptor == null) { + cf.loadTarget(mv); + } + CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); + } + mv.visitLdcInsn(propertyName); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + } + + + /** + * Exception thrown from {@code read} in order to reset a cached + * PropertyAccessor, allowing other accessors to have a try. + */ + @SuppressWarnings("serial") + private static class MapAccessException extends AccessException { + + private final String key; + + public MapAccessException(String key) { + super(""); + this.key = key; + } + + @Override + public String getMessage() { + return "Map does not contain a value for key '" + this.key + "'"; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java index e1a84d991f3f..abc80b037006 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,17 +19,14 @@ import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.expression.AccessException; -import org.springframework.expression.ConstructorExecutor; import org.springframework.expression.ConstructorResolver; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.expression.spel.testresources.Fruit; import org.springframework.expression.spel.testresources.PlaceOfBirth; import static org.assertj.core.api.Assertions.assertThat; @@ -40,56 +37,20 @@ * * @author Andy Clement */ -public class ConstructorInvocationTests extends AbstractExpressionTests { +class ConstructorInvocationTests extends AbstractExpressionTests { @Test - public void testTypeConstructors() { + void constructorWithArgument() { evaluate("new String('hello world')", "hello world", String.class); } @Test - public void testNonExistentType() { + void nonExistentType() { evaluateAndCheckError("new FooBar()", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM); } - - @SuppressWarnings("serial") - static class TestException extends Exception { - - } - - static class Tester { - - public static int counter; - public int i; - - - public Tester() { - } - - public Tester(int i) throws Exception { - counter++; - if (i == 1) { - throw new IllegalArgumentException("IllegalArgumentException for 1"); - } - if (i == 2) { - throw new RuntimeException("RuntimeException for 2"); - } - if (i == 4) { - throw new TestException(); - } - this.i = i; - } - - public Tester(PlaceOfBirth pob) { - - } - - } - - @Test - public void testConstructorThrowingException_SPR6760() { + void constructorThrowingException() { // Test ctor on inventor: // On 1 it will throw an IllegalArgumentException // On 2 it will throw a RuntimeException @@ -100,18 +61,18 @@ public void testConstructorThrowingException_SPR6760() { Expression expr = parser.parseExpression("new org.springframework.expression.spel.ConstructorInvocationTests$Tester(#bar).i"); // Normal exit - StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); - eContext.setRootObject(new Tester()); - eContext.setVariable("bar", 3); - Object o = expr.getValue(eContext); + StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + context.setRootObject(new Tester()); + context.setVariable("bar", 3); + Object o = expr.getValue(context); assertThat(o).isEqualTo(3); - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(1); + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(1); // Now the expression has cached that throwException(int) is the right thing to // call. Let's change 'bar' to be a PlaceOfBirth which indicates the cached // reference is out of date. - eContext.setVariable("bar", new PlaceOfBirth("London")); - o = expr.getValue(eContext); + context.setVariable("bar", new PlaceOfBirth("London")); + o = expr.getValue(context); assertThat(o).isEqualTo(0); // That confirms the logic to mark the cached reference stale and retry is working @@ -119,46 +80,48 @@ public void testConstructorThrowingException_SPR6760() { // a retry. // First, switch back to throwException(int) - eContext.setVariable("bar", 3); - o = expr.getValue(eContext); + context.setVariable("bar", 3); + o = expr.getValue(context); assertThat(o).isEqualTo(3); - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(2); + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(2); // 4 will make it throw a checked exception - this will be wrapped by spel on the // way out - eContext.setVariable("bar", 4); + context.setVariable("bar", 4); assertThatException() - .isThrownBy(() -> expr.getValue(eContext)) + .isThrownBy(() -> expr.getValue(context)) .withMessageContaining("Tester"); // A problem occurred whilst attempting to construct an object of type // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' // using arguments '(java.lang.Integer)' // If counter is 4 then the method got called twice! - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(3); + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(3); // 1 will make it throw a RuntimeException - SpEL will let this through - eContext.setVariable("bar", 1); + context.setVariable("bar", 1); assertThatException() - .isThrownBy(() -> expr.getValue(eContext)) + .isThrownBy(() -> expr.getValue(context)) .isNotInstanceOf(SpelEvaluationException.class); // A problem occurred whilst attempting to construct an object of type // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' // using arguments '(java.lang.Integer)' // If counter is 5 then the method got called twice! - assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(4); + assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(4); } @Test - public void testAddingConstructorResolvers() { + void constructorResolvers() { StandardEvaluationContext ctx = new StandardEvaluationContext(); // reflective constructor accessor is the only one by default List constructorResolvers = ctx.getConstructorResolvers(); assertThat(constructorResolvers).hasSize(1); - ConstructorResolver dummy = new DummyConstructorResolver(); + ConstructorResolver dummy = (context, typeName, argumentTypes) -> { + throw new UnsupportedOperationException(); + }; ctx.addConstructorResolver(dummy); assertThat(ctx.getConstructorResolvers()).hasSize(2); @@ -171,48 +134,29 @@ public void testAddingConstructorResolvers() { assertThat(ctx.getConstructorResolvers()).hasSize(2); } - - static class DummyConstructorResolver implements ConstructorResolver { - - @Override - public ConstructorExecutor resolve(EvaluationContext context, String typeName, - List argumentTypes) throws AccessException { - throw new UnsupportedOperationException("Auto-generated method stub"); - } - - } - - @Test - public void testVarargsInvocation01() { - // Calling 'Fruit(String... strings)' - evaluate("new org.springframework.expression.spel.testresources.Fruit('a','b','c').stringscount()", 3, - Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit('a').stringscount()", 1, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit().stringscount()", 0, Integer.class); + void varargsConstructors() { + ((StandardTypeLocator) super.context.getTypeLocator()).registerImport(Fruit.class.getPackageName()); + + // Calling 'Fruit(String... strings)' - returns length_of_strings + evaluate("new Fruit('a','b','c').stringscount()", 3, Integer.class); + evaluate("new Fruit('a').stringscount()", 1, Integer.class); + evaluate("new Fruit().stringscount()", 0, Integer.class); // all need converting to strings - evaluate("new org.springframework.expression.spel.testresources.Fruit(1,2,3).stringscount()", 3, Integer.class); + evaluate("new Fruit(1,2,3).stringscount()", 3, Integer.class); // needs string conversion - evaluate("new org.springframework.expression.spel.testresources.Fruit(1).stringscount()", 1, Integer.class); + evaluate("new Fruit(1).stringscount()", 1, Integer.class); // first and last need conversion - evaluate("new org.springframework.expression.spel.testresources.Fruit(1,'a',3.0d).stringscount()", 3, - Integer.class); - } - - @Test - public void testVarargsInvocation02() { - // Calling 'Fruit(int i, String... strings)' - returns int+length_of_strings - evaluate("new org.springframework.expression.spel.testresources.Fruit(5,'a','b','c').stringscount()", 8, - Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a').stringscount()", 3, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(4).stringscount()", 4, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(8,2,3).stringscount()", 10, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(9).stringscount()", 9, Integer.class); - evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a',3.0d).stringscount()", 4, - Integer.class); - evaluate( - "new org.springframework.expression.spel.testresources.Fruit(8,stringArrayOfThreeItems).stringscount()", - 11, Integer.class); + evaluate("new Fruit(1,'a',3.0d).stringscount()", 3, Integer.class); + + // Calling 'Fruit(int i, String... strings)' - returns int + length_of_strings + evaluate("new Fruit(5,'a','b','c').stringscount()", 8, Integer.class); + evaluate("new Fruit(2,'a').stringscount()", 3, Integer.class); + evaluate("new Fruit(4).stringscount()", 4, Integer.class); + evaluate("new Fruit(8,2,3).stringscount()", 10, Integer.class); + evaluate("new Fruit(9).stringscount()", 9, Integer.class); + evaluate("new Fruit(2,'a',3.0d).stringscount()", 4, Integer.class); + evaluate("new Fruit(8,stringArrayOfThreeItems).stringscount()", 11, Integer.class); } /* @@ -220,7 +164,7 @@ public void testVarargsInvocation02() { * the argument in order to satisfy a suitable constructor. */ @Test - public void testWidening01() { + void widening() { // widening of int 3 to double 3 is OK evaluate("new Double(3)", 3.0d, Double.class); // widening of int 3 to long 3 is OK @@ -228,12 +172,40 @@ public void testWidening01() { } @Test - @Disabled - public void testArgumentConversion01() { - // Closest ctor will be new String(String) and converter supports Double>String - // TODO currently failing as with new ObjectToArray converter closest constructor - // matched becomes String(byte[]) which fails... + void argumentConversion() { evaluate("new String(3.0d)", "3.0", String.class); } + + @SuppressWarnings("serial") + static class TestException extends Exception { + } + + static class Tester { + + public static int counter; + public int i; + + + public Tester() { + } + + public Tester(int i) throws Exception { + counter++; + if (i == 1) { + throw new IllegalArgumentException("IllegalArgumentException for 1"); + } + if (i == 2) { + throw new RuntimeException("RuntimeException for 2"); + } + if (i == 4) { + throw new TestException(); + } + this.i = i; + } + + public Tester(PlaceOfBirth pob) { + } + } + } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index 43d589116acf..c0733d072391 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -47,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.within; +import static org.springframework.expression.spel.SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST; /** * Tests the evaluation of real expressions in a real context. @@ -109,8 +109,8 @@ void createListsOnAttemptToIndexNull01() throws EvaluationException, ParseExcept assertThat(o).isEqualTo(""); assertThat(testClass.list).hasSize(4); - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - parser.parseExpression("list2[3]").getValue(new StandardEvaluationContext(testClass))); + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(() -> parser.parseExpression("list2[3]").getValue(new StandardEvaluationContext(testClass))); o = parser.parseExpression("foo[3]").getValue(new StandardEvaluationContext(testClass)); assertThat(o).isEqualTo(""); @@ -128,8 +128,8 @@ void createMapsOnAttemptToIndexNull() { o = parser.parseExpression("map").getValue(ctx); assertThat(o).isNotNull(); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("map2['a']").getValue(ctx)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> parser.parseExpression("map2['a']").getValue(ctx)); // map2 should be null, there is no setter } @@ -145,8 +145,8 @@ void createObjectsOnAttemptToReferenceNull() { o = parser.parseExpression("wibble").getValue(ctx); assertThat(o).isNotNull(); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("wibble2.bar").getValue(ctx)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> parser.parseExpression("wibble2.bar").getValue(ctx)); } @Test @@ -234,7 +234,7 @@ void indexerError() { @Test void stringType() { - evaluateAndAskForReturnType("getPlaceOfBirth().getCity()", "SmilJan", String.class); + evaluateAndAskForReturnType("getPlaceOfBirth().getCity()", "Smiljan", String.class); } @Test @@ -271,8 +271,8 @@ void comparison() { @Test void resolvingList() { StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(() -> parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); ((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util"); assertThat(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)).isTrue(); } @@ -303,12 +303,12 @@ void initializingCollectionElementsOnWrite() { e = parser.parseExpression("address.crossStreets[0]"); e.setValue(context, "Blah"); - assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); + assertThat(person.getAddress().getCrossStreets()).element(0).isEqualTo("Blah"); e = parser.parseExpression("address.crossStreets[3]"); e.setValue(context, "Wibble"); - assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); - assertThat(person.getAddress().getCrossStreets().get(3)).isEqualTo("Wibble"); + assertThat(person.getAddress().getCrossStreets()).element(0).isEqualTo("Blah"); + assertThat(person.getAddress().getCrossStreets()).element(3).isEqualTo("Wibble"); } /** @@ -336,7 +336,7 @@ void customMethodFilter() { StandardEvaluationContext context = new StandardEvaluationContext(); // Register a custom MethodResolver... - context.setMethodResolvers(Arrays.asList((evaluationContext, targetObject, name, argumentTypes) -> null)); + context.setMethodResolvers(List.of((evaluationContext, targetObject, name, argumentTypes) -> null)); // or simply... // context.setMethodResolvers(new ArrayList()); @@ -344,8 +344,8 @@ void customMethodFilter() { // Register a custom MethodFilter... MethodFilter methodFilter = methods -> null; assertThatIllegalStateException() - .isThrownBy(() -> context.registerMethodFilter(String.class, methodFilter)) - .withMessage("Method filter cannot be set as the reflective method resolver is not in use"); + .isThrownBy(() -> context.registerMethodFilter(String.class, methodFilter)) + .withMessage("Method filter cannot be set as the reflective method resolver is not in use"); } /** @@ -359,21 +359,21 @@ void collectionGrowingViaIndexer() { // Add a new element to the list StandardEvaluationContext ctx = new StandardEvaluationContext(instance); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e = parser.parseExpression("listOfStrings[++index3]='def'"); + Expression e = parser.parseExpression("listOfStrings[++index3]='def'"); e.getValue(ctx); assertThat(instance.listOfStrings).hasSize(2); - assertThat(instance.listOfStrings.get(1)).isEqualTo("def"); + assertThat(instance.listOfStrings).element(1).isEqualTo("def"); // Check reference beyond end of collection ctx = new StandardEvaluationContext(instance); parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - e = parser.parseExpression("listOfStrings[0]"); + e = parser.parseExpression("listOfStrings[0]"); String value = e.getValue(ctx, String.class); assertThat(value).isEqualTo("abc"); - e = parser.parseExpression("listOfStrings[1]"); + e = parser.parseExpression("listOfStrings[1]"); value = e.getValue(ctx, String.class); assertThat(value).isEqualTo("def"); - e = parser.parseExpression("listOfStrings[2]"); + e = parser.parseExpression("listOfStrings[2]"); value = e.getValue(ctx, String.class); assertThat(value).isEmpty(); @@ -381,9 +381,9 @@ void collectionGrowingViaIndexer() { StandardEvaluationContext failCtx = new StandardEvaluationContext(instance); parser = new SpelExpressionParser(new SpelParserConfiguration(false, false)); Expression failExp = parser.parseExpression("listOfStrings[3]"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - failExp.getValue(failCtx, String.class)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> failExp.getValue(failCtx, String.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS)); } @Test @@ -547,6 +547,18 @@ void matchesWithPatternLengthThreshold() { evaluateAndCheckError("'X' matches '" + pattern + "'", Boolean.class, SpelMessage.MAX_REGEX_LENGTH_EXCEEDED); } + @Test + void betweenOperator() { + evaluate("1 between listOneFive", "true", Boolean.class); + evaluate("1 between {1, 5}", "true", Boolean.class); + } + + @Test + void betweenOperatorErrors() { + evaluateAndCheckError("1 between T(String)", BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); + evaluateAndCheckError("1 between listOfNumbersUpToTen", BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); + } + } @Nested @@ -554,7 +566,7 @@ class PropertyAccessTests { @Test void propertyField() { - evaluate("name", "Nikola Tesla", String.class, false); + evaluate("name", "Nikola Tesla", String.class, true); // not writable because (1) name is private (2) there is no setter, only a getter evaluateAndCheckError("madeup", SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, 0, "madeup", "org.springframework.expression.spel.testresources.Inventor"); @@ -568,12 +580,12 @@ void propertyField_SPR7100() { @Test void rogueTrailingDotCausesNPE_SPR6866() { - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> - new SpelExpressionParser().parseExpression("placeOfBirth.foo.")) - .satisfies(ex -> { - assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OOD); - assertThat(ex.getPosition()).isEqualTo(16); - }); + assertThatExceptionOfType(SpelParseException.class) + .isThrownBy(() -> new SpelExpressionParser().parseExpression("placeOfBirth.foo.")) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OOD); + assertThat(ex.getPosition()).isEqualTo(16); + }); } @Nested @@ -582,7 +594,7 @@ class NestedPropertiesTests { // nested properties @Test void propertiesNested01() { - evaluate("placeOfBirth.city", "SmilJan", String.class, true); + evaluate("placeOfBirth.city", "Smiljan", String.class, true); } @Test @@ -592,12 +604,12 @@ void propertiesNested02() { @Test void propertiesNested03() throws ParseException { - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> - new SpelExpressionParser().parseRaw("placeOfBirth.23")) - .satisfies(ex -> { - assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.UNEXPECTED_DATA_AFTER_DOT); - assertThat(ex.getInserts()[0]).isEqualTo("23"); - }); + assertThatExceptionOfType(SpelParseException.class) + .isThrownBy(() -> new SpelExpressionParser().parseRaw("placeOfBirth.23")) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.UNEXPECTED_DATA_AFTER_DOT); + assertThat(ex.getInserts()[0]).isEqualTo("23"); + }); } } @@ -681,26 +693,26 @@ class BinaryOperatorTests { @Test void andWithNullValueOnLeft() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("null and true")::getValue); + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(parser.parseExpression("null and true")::getValue); } @Test void andWithNullValueOnRight() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("true and null")::getValue); + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(parser.parseExpression("true and null")::getValue); } @Test void orWithNullValueOnLeft() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("null or false")::getValue); + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(parser.parseExpression("null or false")::getValue); } @Test void orWithNullValueOnRight() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("false or null")::getValue); + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(parser.parseExpression("false or null")::getValue); } } @@ -758,8 +770,8 @@ void ternaryExpressionWithExplicitGrouping() { @Test void ternaryOperatorWithNullValue() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy( - parser.parseExpression("null ? 0 : 1")::getValue); + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(parser.parseExpression("null ? 0 : 1")::getValue); } } @@ -858,11 +870,11 @@ void increment01root() { Integer i = 42; StandardEvaluationContext ctx = new StandardEvaluationContext(i); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e = parser.parseExpression("#this++"); + Expression e = parser.parseExpression("#this++"); assertThat(i).isEqualTo(42); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e.getValue(ctx, Integer.class)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); } @Test @@ -876,44 +888,44 @@ void increment02postfix() { e = parser.parseExpression("bd++"); assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); - assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("2")).isEqualTo(return_bd); assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); // double e = parser.parseExpression("ddd++"); assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); + double return_ddd = e.getValue(ctx, double.class); assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); // float e = parser.parseExpression("fff++"); assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); + float return_fff = e.getValue(ctx, float.class); assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); // long e = parser.parseExpression("lll++"); assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); + long return_lll = e.getValue(ctx, long.class); assertThat(return_lll).isEqualTo(66666L); assertThat(helper.lll).isEqualTo(66667L); // int e = parser.parseExpression("iii++"); assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); + int return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(42); assertThat(helper.iii).isEqualTo(43); - return_iii = e.getValue(ctx, Integer.TYPE); + return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(43); assertThat(helper.iii).isEqualTo(44); // short e = parser.parseExpression("sss++"); assertThat(helper.sss).isEqualTo((short) 15); - short return_sss = e.getValue(ctx, Short.TYPE); + short return_sss = e.getValue(ctx, short.class); assertThat(return_sss).isEqualTo((short) 15); assertThat(helper.sss).isEqualTo((short) 16); } @@ -929,37 +941,37 @@ void increment02prefix() { e = parser.parseExpression("++bd"); assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); - assertThat(new BigDecimal("3").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("3")).isEqualTo(return_bd); assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); // double e = parser.parseExpression("++ddd"); assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); + double return_ddd = e.getValue(ctx, double.class); assertThat((float) return_ddd).isCloseTo((float) 3.0d, within((float) 0d)); assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); // float e = parser.parseExpression("++fff"); assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); + float return_fff = e.getValue(ctx, float.class); assertThat(return_fff).isCloseTo(4.0f, within((float) 0d)); assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); // long e = parser.parseExpression("++lll"); assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); + long return_lll = e.getValue(ctx, long.class); assertThat(return_lll).isEqualTo(66667L); assertThat(helper.lll).isEqualTo(66667L); // int e = parser.parseExpression("++iii"); assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); + int return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(43); assertThat(helper.iii).isEqualTo(43); - return_iii = e.getValue(ctx, Integer.TYPE); + return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(44); assertThat(helper.iii).isEqualTo(44); @@ -978,14 +990,14 @@ void increment03() { ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); Expression e1 = parser.parseExpression("m()++"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Double.TYPE)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e1.getValue(ctx, double.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); Expression e2 = parser.parseExpression("++m()"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Double.TYPE)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e2.getValue(ctx, double.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); } @Test @@ -993,14 +1005,14 @@ void increment04() { Integer i = 42; StandardEvaluationContext ctx = new StandardEvaluationContext(i); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e1 = parser.parseExpression("++1"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Double.TYPE)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); - Expression e2 = parser.parseExpression("1++"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Double.TYPE)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + Expression e1 = parser.parseExpression("++1"); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e1.getValue(ctx, double.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + Expression e2 = parser.parseExpression("1++"); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e2.getValue(ctx, double.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); } @Test @@ -1008,11 +1020,11 @@ void decrement01root() { Integer i = 42; StandardEvaluationContext ctx = new StandardEvaluationContext(i); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); - Expression e = parser.parseExpression("#this--"); + Expression e = parser.parseExpression("#this--"); assertThat(i).isEqualTo(42); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e.getValue(ctx, Integer.class)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); } @Test @@ -1026,44 +1038,44 @@ void decrement02postfix() { e = parser.parseExpression("bd--"); assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); - assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("2")).isEqualTo(return_bd); assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); // double e = parser.parseExpression("ddd--"); assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); + double return_ddd = e.getValue(ctx, double.class); assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); // float e = parser.parseExpression("fff--"); assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); + float return_fff = e.getValue(ctx, float.class); assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); // long e = parser.parseExpression("lll--"); assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); + long return_lll = e.getValue(ctx, long.class); assertThat(return_lll).isEqualTo(66666L); assertThat(helper.lll).isEqualTo(66665L); // int e = parser.parseExpression("iii--"); assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); + int return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(42); assertThat(helper.iii).isEqualTo(41); - return_iii = e.getValue(ctx, Integer.TYPE); + return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(41); assertThat(helper.iii).isEqualTo(40); // short e = parser.parseExpression("sss--"); assertThat(helper.sss).isEqualTo((short) 15); - short return_sss = e.getValue(ctx, Short.TYPE); + short return_sss = e.getValue(ctx, short.class); assertThat(return_sss).isEqualTo((short) 15); assertThat(helper.sss).isEqualTo((short) 14); } @@ -1079,37 +1091,37 @@ void decrement02prefix() { e = parser.parseExpression("--bd"); assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); - assertThat(new BigDecimal("1").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("1")).isEqualTo(return_bd); assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); // double e = parser.parseExpression("--ddd"); assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); - double return_ddd = e.getValue(ctx, Double.TYPE); + double return_ddd = e.getValue(ctx, double.class); assertThat((float) return_ddd).isCloseTo((float) 1.0d, within((float) 0d)); assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); // float e = parser.parseExpression("--fff"); assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); - float return_fff = e.getValue(ctx, Float.TYPE); + float return_fff = e.getValue(ctx, float.class); assertThat(return_fff).isCloseTo(2.0f, within((float) 0d)); assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); // long e = parser.parseExpression("--lll"); assertThat(helper.lll).isEqualTo(66666L); - long return_lll = e.getValue(ctx, Long.TYPE); + long return_lll = e.getValue(ctx, long.class); assertThat(return_lll).isEqualTo(66665L); assertThat(helper.lll).isEqualTo(66665L); // int e = parser.parseExpression("--iii"); assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); + int return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(41); assertThat(helper.iii).isEqualTo(41); - return_iii = e.getValue(ctx, Integer.TYPE); + return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(40); assertThat(helper.iii).isEqualTo(40); @@ -1128,14 +1140,14 @@ void decrement03() { ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); Expression e1 = parser.parseExpression("m()--"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Double.TYPE)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e1.getValue(ctx, double.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); Expression e2 = parser.parseExpression("--m()"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Double.TYPE)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e2.getValue(ctx, double.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); } @Test @@ -1144,14 +1156,14 @@ void decrement04() { StandardEvaluationContext ctx = new StandardEvaluationContext(i); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); Expression e1 = parser.parseExpression("--1"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e1.getValue(ctx, Integer.class)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e1.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); Expression e2 = parser.parseExpression("1--"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - e2.getValue(ctx, Integer.class)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> e2.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); } @Test @@ -1169,13 +1181,13 @@ void incrementAndDecrementTogether() { assertThat(helper.intArray[2]).isEqualTo(4); // index1 is 3 intArray[3] is 4 - e = parser.parseExpression("intArray[#root.index1++]--"); + e = parser.parseExpression("intArray[#root.index1++]--"); assertThat(e.getValue(ctx, Integer.class)).isEqualTo(4); assertThat(helper.index1).isEqualTo(4); assertThat(helper.intArray[3]).isEqualTo(3); // index1 is 4, intArray[3] is 3 - e = parser.parseExpression("intArray[--#root.index1]++"); + e = parser.parseExpression("intArray[--#root.index1]++"); assertThat(e.getValue(ctx, Integer.class)).isEqualTo(3); assertThat(helper.index1).isEqualTo(3); assertThat(helper.intArray[3]).isEqualTo(4); @@ -1337,26 +1349,26 @@ void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { // iii=42 e = parser.parseExpression("iii=iii++"); assertThat(helper.iii).isEqualTo(42); - int return_iii = e.getValue(ctx, Integer.TYPE); + int return_iii = e.getValue(ctx, int.class); assertThat(helper.iii).isEqualTo(42); assertThat(return_iii).isEqualTo(42); // Identifier e = parser.parseExpression("iii++"); assertThat(helper.iii).isEqualTo(42); - return_iii = e.getValue(ctx, Integer.TYPE); + return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(42); assertThat(helper.iii).isEqualTo(43); e = parser.parseExpression("--iii"); assertThat(helper.iii).isEqualTo(43); - return_iii = e.getValue(ctx, Integer.TYPE); + return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(42); assertThat(helper.iii).isEqualTo(42); e = parser.parseExpression("iii=99"); assertThat(helper.iii).isEqualTo(42); - return_iii = e.getValue(ctx, Integer.TYPE); + return_iii = e.getValue(ctx, int.class); assertThat(return_iii).isEqualTo(99); assertThat(helper.iii).isEqualTo(99); @@ -1364,19 +1376,19 @@ void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { // foo.iii == 99 e = parser.parseExpression("foo.iii++"); assertThat(helper.foo.iii).isEqualTo(99); - int return_foo_iii = e.getValue(ctx, Integer.TYPE); + int return_foo_iii = e.getValue(ctx, int.class); assertThat(return_foo_iii).isEqualTo(99); assertThat(helper.foo.iii).isEqualTo(100); e = parser.parseExpression("--foo.iii"); assertThat(helper.foo.iii).isEqualTo(100); - return_foo_iii = e.getValue(ctx, Integer.TYPE); + return_foo_iii = e.getValue(ctx, int.class); assertThat(return_foo_iii).isEqualTo(99); assertThat(helper.foo.iii).isEqualTo(99); e = parser.parseExpression("foo.iii=999"); assertThat(helper.foo.iii).isEqualTo(99); - return_foo_iii = e.getValue(ctx, Integer.TYPE); + return_foo_iii = e.getValue(ctx, int.class); assertThat(return_foo_iii).isEqualTo(999); assertThat(helper.foo.iii).isEqualTo(999); @@ -1396,7 +1408,7 @@ void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { expectFailSetValueNotSupported(parser, ctx, "('abc' matches '^a..')=('abc' matches '^a..')"); // Selection - ctx.registerFunction("isEven", Spr9751.class.getDeclaredMethod("isEven", Integer.TYPE)); + ctx.registerFunction("isEven", Spr9751.class.getDeclaredMethod("isEven", int.class)); expectFailNotIncrementable(parser, ctx, "({1,2,3}.?[#isEven(#this)])++"); expectFailNotDecrementable(parser, ctx, "--({1,2,3}.?[#isEven(#this)])"); @@ -1428,19 +1440,19 @@ void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { ctx.setVariable("wobble", 3); e = parser.parseExpression("#wobble++"); assertThat(((Integer) ctx.lookupVariable("wobble"))).isEqualTo(3); - int r = e.getValue(ctx, Integer.TYPE); + int r = e.getValue(ctx, int.class); assertThat(r).isEqualTo(3); assertThat(((Integer) ctx.lookupVariable("wobble"))).isEqualTo(4); e = parser.parseExpression("--#wobble"); assertThat(((Integer) ctx.lookupVariable("wobble"))).isEqualTo(4); - r = e.getValue(ctx, Integer.TYPE); + r = e.getValue(ctx, int.class); assertThat(r).isEqualTo(3); assertThat(((Integer) ctx.lookupVariable("wobble"))).isEqualTo(3); e = parser.parseExpression("#wobble=34"); assertThat(((Integer) ctx.lookupVariable("wobble"))).isEqualTo(3); - r = e.getValue(ctx, Integer.TYPE); + r = e.getValue(ctx, int.class); assertThat(r).isEqualTo(34); assertThat(((Integer) ctx.lookupVariable("wobble"))).isEqualTo(34); @@ -1475,19 +1487,19 @@ void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { helper.iii = 42; e = parser.parseExpression("iii++"); assertThat(helper.iii).isEqualTo(42); - r = e.getValue(ctx, Integer.TYPE); + r = e.getValue(ctx, int.class); assertThat(r).isEqualTo(42); assertThat(helper.iii).isEqualTo(43); e = parser.parseExpression("--iii"); assertThat(helper.iii).isEqualTo(43); - r = e.getValue(ctx, Integer.TYPE); + r = e.getValue(ctx, int.class); assertThat(r).isEqualTo(42); assertThat(helper.iii).isEqualTo(42); e = parser.parseExpression("iii=100"); assertThat(helper.iii).isEqualTo(42); - r = e.getValue(ctx, Integer.TYPE); + r = e.getValue(ctx, int.class); assertThat(r).isEqualTo(100); assertThat(helper.iii).isEqualTo(100); } @@ -1509,13 +1521,15 @@ private void expectFailNotDecrementable(ExpressionParser parser, EvaluationConte } private void expectFail(ExpressionParser parser, EvaluationContext eContext, String expressionString, SpelMessage messageCode) { - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> { - Expression e = parser.parseExpression(expressionString); - if (DEBUG) { - SpelUtilities.printAbstractSyntaxTree(System.out, e); - } - e.getValue(eContext); - }).satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode)); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> { + Expression e = parser.parseExpression(expressionString); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, e); + } + e.getValue(eContext); + }) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode)); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionLanguageScenarioTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionLanguageScenarioTests.java index aa6bf033f52e..a72b95e43e98 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionLanguageScenarioTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionLanguageScenarioTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,13 @@ import java.awt.Color; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; -import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; -import org.springframework.expression.ParseException; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -37,10 +33,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -///CLOVER:OFF - /** - * Testcases showing the common scenarios/use-cases for picking up the expression language support. + * Test cases showing the common scenarios/use-cases for picking up the expression language support. * The first test shows very basic usage, just drop it in and go. By 'standard infrastructure', it means:
      *